package com.bestcem.xm.survey.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.useragent.Platform;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bestcem.bp.xm.user.grpc.v1.services.Organization;
import com.bestcem.xm.common.core.enums.ReqErrorCodeEnum;
import com.bestcem.xm.common.core.exception.BusinessException;
import com.bestcem.xm.common.core.uitls.ExcelExportUtil;
import com.bestcem.xm.common.core.uitls.SignUtil;
import com.bestcem.xm.componet.filestorage.service.StorageService;
import com.bestcem.xm.componet.redis.service.RedisService;
import com.bestcem.xm.qdes.grpc.v1.services.Project;
import com.bestcem.xm.qdes.grpc.v1.services.Question;
import com.bestcem.xm.survey.controller.vo.SurveyColumnVo;
import com.bestcem.xm.survey.controller.vo.SurveyQuotaCheckItemVo;
import com.bestcem.xm.survey.convert.SurveyConvertMapper;
import com.bestcem.xm.survey.convert.grpc.SurveyGrpcConvertMapper;
import com.bestcem.xm.survey.entity.*;
import com.bestcem.xm.survey.enums.ExtparamDtypeDataEnum;
import com.bestcem.xm.survey.enums.RespondentSourceEnum;
import com.bestcem.xm.survey.enums.RespondentStatusEnum;
import com.bestcem.xm.survey.enums.SurveyErrorCodeEnum;
import com.bestcem.xm.survey.grpc.client.*;
import com.bestcem.xm.survey.grpc.client.dto.*;
import com.bestcem.xm.survey.mapper.*;
import com.bestcem.xm.survey.mq.send.biz.SurveyMessageSendService;
import com.bestcem.xm.survey.service.*;
import com.bestcem.xm.survey.util.Ip2RegionUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
import org.javatuples.Pair;
import org.javatuples.Quartet;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.*;
import java.util.stream.Collectors;

import static com.bestcem.xm.deliver.grpc.v1.services.CustomLinkDeliverParamOnlyAgingType.CUSTOMLINKDELIVER_PARAM_ONLY_AGING_TYPE_PARAMS_PERMANENT_VALUE;
import static com.bestcem.xm.deliver.grpc.v1.services.CustomLinkDeliverParamOnlyAgingType.CUSTOMLINKDELIVER_PARAM_ONLY_AGING_TYPE_PARAMS_SPECIFIED_VALUE;
import static com.bestcem.xm.deliver.grpc.v1.services.DefaultDeliverStatus.DEFAULTDELIVER_STATUS_DISACTIVATE_VALUE;
import static com.bestcem.xm.deliver.grpc.v1.services.DeliverExtparamsStatus.DELIVEREXTPARAMS_STATUS_START;
import static com.bestcem.xm.deliver.grpc.v1.services.DeliverExtparamsStatus.DELIVEREXTPARAMS_STATUS_START_VALUE;
import static com.bestcem.xm.qdes.grpc.v1.services.NameListNtype.NAMELIST_NTYPE_BLACK_VALUE;
import static com.bestcem.xm.qdes.grpc.v1.services.NameListNtype.NAMELIST_NTYPE_WHITE_VALUE;
import static com.bestcem.xm.survey.enums.MemberFootprintTypeEnum.MEMBER_FOOTPRINT_TYPE_SURVEY;
import static com.bestcem.xm.survey.enums.RespondentDataStatusEnum.RESPONDENT_DATA_STATUS_FORMAL;
import static com.bestcem.xm.survey.grpc.v1.services.ColumnCtype.COLUMN_CTYPE_EXTERNAL;
import static com.bestcem.xm.survey.grpc.v1.services.ColumnStatus.COLUMN_STATUS_DELETE;
import static com.bestcem.xm.survey.grpc.v1.services.ConditionRule.*;
import static com.bestcem.xm.survey.grpc.v1.services.FilterAction.FILTER_ACTION_FITDISCARD_VALUE;
import static com.bestcem.xm.survey.grpc.v1.services.FilterAction.FILTER_ACTION_UNFITDISCARD_VALUE;
import static com.bestcem.xm.survey.grpc.v1.services.FilterStatus.FILTER_STATUS_OPEN_VALUE;
import static com.bestcem.xm.survey.grpc.v1.services.RespondentClientType.*;
import static com.bestcem.xm.survey.grpc.v1.services.RespondentSource.RESPONDENT_SOURCE_PREVIEW_VALUE;
import static com.bestcem.xm.survey.grpc.v1.services.RespondentSource.RESPONDENT_SOURCE_UNKNOWN_VALUE;
import static com.bestcem.xm.survey.grpc.v1.services.RespondentStatus.*;
import static com.bestcem.xm.survey.grpc.v1.services.RespondentWeiXinWxSex.RESPONDENTWEIXIN_WX_SEX_UNKNOWN_VALUE;
import static com.bestcem.xm.survey.util.SurveyConstant.*;

/**
 * 公用的代码写这里
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class SurveyCommonServiceImpl implements SurveyCommonService {

    private @NonNull RedisService redisService;
    private @NonNull ServiceSequenceService serviceSequenceService;
    private @NonNull SurveyExtParameterService surveyExtParameterService;
    private @NonNull SurveyUniqueAnswerService surveyUniqueAnswerService;
    private @NonNull SurveyCustomDataService surveyCustomDataService;
    private @NonNull SurveyQuotaConditionService surveyQuotaConditionService;
    private @NonNull SurveyRespondentService surveyRespondentService;
    private @NonNull SurveyRespondentWeixinService surveyRespondentWeixinService;
    private @NonNull SurveyRespondentExtraService surveyRespondentExtraService;
    private @NonNull SurveyMessageSendService surveyMessageSendService;
    private @NonNull StorageService storageService;

    private @NonNull SurveyConvertMapper surveyConvertMapper;
    private @NonNull SurveyGrpcConvertMapper surveyGrpcConvertMapper;
    private @NonNull SurveyRespondentCountMapper surveyRespondentCountMapper;

    private @NonNull SurveyRespondentMapper surveyRespondentMapper;
    private @NonNull SurveyRespondentDataMapper surveyRespondentDataMapper;
    private @NonNull SurveyRespondentAttachmentMapper surveyRespondentAttachmentMapper;
    private @NonNull SurveyColumnMapper surveyColumnMapper;
    private @NonNull SurveyRespondentPropDataMapper surveyRespondentPropDataMapper;
    private @NonNull SurveyDeliverUniqueAnswerMapper surveyDeliverUniqueAnswerMapper;
    private @NonNull SurveyRespondentWeixinMapper surveyRespondentWeixinMapper;
    private @NonNull SurveyCustomMetaMapper surveyCustomMetaMapper;
    private @NonNull SurveyFilterMapper surveyFilterMapper;
    private @NonNull SurveyConditionMapper surveyConditionMapper;
    private @NonNull SurveyQuotaConditionMapper surveyQuotaConditionMapper;
    private @NonNull SurveyQuotaCheckItemMapper surveyQuotaCheckItemMapper;
    private @NonNull SurveyRespondentExtraMapper surveyRespondentExtraMapper;
    private @NonNull SurveyRespondentHashQuestionListMapper surveyRespondentHashQuestionListMapper;

    private @NonNull QdesGrpcClient qdesGrpcClient;
    private @NonNull UserGrpcClient userGrpcClient;
    private @NonNull DeliverGrpcClient deliverGrpcClient;
    private @NonNull OpenPlatformGrpcClient openPlatformGrpcClient;
    private @NonNull PushGrpcClient pushGrpcClient;

    private @NonNull ObjectMapper objectMapper;

    @Resource(name = "saveRspdPropTaskExecutor")
    private ThreadPoolTaskExecutor saveRspdPropTaskExecutor;

    @Value("${app.survey.real-url}")
    private String surveyDomain;

    @Value("${app.survey.sign.deliver-secret}")
    private String deliverSecret;

    @Value("${app.survey.sign.survey-secret}")
    private String surveySecret;

    private final ConcurrentHashMap<String, BiFunction<Object, Object, Boolean>> COND_MATCHER = new ConcurrentHashMap<String, BiFunction<Object, Object, Boolean>>() {
        {
            put("gt", (act_v, exp_v) -> Integer.parseInt(act_v.toString()) > Integer.parseInt(exp_v.toString()));
            put("ge", (act_v, exp_v) -> Integer.parseInt(act_v.toString()) >= Integer.parseInt(exp_v.toString()));
            put("lt", (act_v, exp_v) -> Integer.parseInt(act_v.toString()) < Integer.parseInt(exp_v.toString()));
            put("le", (act_v, exp_v) -> Integer.parseInt(act_v.toString()) <= Integer.parseInt(exp_v.toString()));
            put("eq", (act_v, exp_v) -> Integer.parseInt(act_v.toString()) == Integer.parseInt(exp_v.toString()));
            put("range", (act_v, exp_v) -> !ObjectUtils.isEmpty(exp_v) && doCondMatch(act_v, exp_v));
        }

        private Boolean doCondMatch(Object act_v, Object exp_v) {
            if (act_v == null || !(exp_v instanceof Map)) {
                return false;
            }
            List<Boolean> list = new ArrayList<>();
            JSONObject value = (JSONObject) exp_v;
            value.forEach((op, v) -> {
                if (COND_MATCHER.containsKey(op)) {
                    list.add(COND_MATCHER.get(op).apply(act_v, v));
                }
            });
            if (!list.isEmpty()) {
                int size = list.size();
                Boolean[] all = list.toArray(new Boolean[size]);
                return BooleanUtil.andOfWrap(all);
            } else {
                return false;
            }
        }
    };

    private final ConcurrentHashMap<String, BiFunction<Object, Object, Boolean>> FILT_MATCHER = new ConcurrentHashMap<String, BiFunction<Object, Object, Boolean>>() {
        {
            put("eq", (x, y) -> handle(y, getBlank(x), (left_y, right_x) -> StrUtil.equals(left_y.toString(), right_x.toString())));
            put("ne", (x, y) -> handle(y, getBlank(x), (left_y, right_x) -> !StrUtil.equals(left_y.toString(), right_x.toString())));
            put("co", (x, y) -> handle(y, getBlank(x), (left_y, right_x) -> {
                //对照数据结构，这里是字符串
                return right_x.toString().contains(left_y.toString());
                /*List<String> x1 = JSONArray.parseArray(JSON.toJSONString(right_x)).toJavaList(String.class);
                return x1.contains(left_y.toString());*/
                /*if (left_y instanceof List) {
                    List<String> y1 = JSONArray.parseArray(JSON.toJSONString(left_y)).toJavaList(String.class);
                    return org.apache.commons.collections4.CollectionUtils.containsAll(x1, y1);
                } else {
                    return x1.contains(left_y.toString());
                }*/
            }));
            put("nc", (x, y) -> handle(y, getBlank(x), (left_y, right_x) -> {
                //对照数据结构，这里是字符串
                return !right_x.toString().contains(left_y.toString());
                /*List<String> x1 = JSONArray.parseArray(JSON.toJSONString(right_x)).toJavaList(String.class);
                return !x1.contains(left_y.toString());*/
                /*if (left_y instanceof List) {
                    List<String> y1 = JSONArray.parseArray(JSON.toJSONString(left_y)).toJavaList(String.class);
                    return !org.apache.commons.collections4.CollectionUtils.containsAll(x1, y1);
                } else {
                    return !x1.contains(left_y.toString());
                }*/
            }));
            put("select", (x, y) -> handle(y, getOptions(x), (left_y, right_x) -> {
                List<String> x1;
                if (right_x instanceof List) {
                    x1 = JSONArray.parseArray(JSON.toJSONString(right_x)).toJavaList(String.class);
                } else {
                    x1 = new ArrayList<>();
                    x1.add(right_x.toString());
                }

                List<String> y1;
                if (left_y instanceof List) {
                    y1 = JSONArray.parseArray(JSON.toJSONString(left_y)).toJavaList(String.class);
                } else {
                    y1 = new ArrayList<>();
                    y1.add(left_y.toString());
                }
                if (ObjectUtils.isEmpty(x1) || ObjectUtils.isEmpty(y1)) {
                    return false;
                }
                Collection<String> intersection = org.apache.commons.collections4.CollectionUtils.intersection(y1, x1);
                return !ObjectUtils.isEmpty(intersection);
            }));
            put("gt", (act_v, exp_v) -> handle(getBlank(act_v), exp_v, (y, x) -> Integer.parseInt(y.toString()) < Integer.parseInt(x.toString())));
            put("ge", (act_v, exp_v) -> handle(getBlank(act_v), exp_v, (y, x) -> Integer.parseInt(y.toString()) <= Integer.parseInt(x.toString())));
            put("lt", (act_v, exp_v) -> handle(getBlank(act_v), exp_v, (y, x) -> Integer.parseInt(y.toString()) > Integer.parseInt(x.toString())));
            put("le", (act_v, exp_v) -> handle(getBlank(act_v), exp_v, (y, x) -> Integer.parseInt(y.toString()) >= Integer.parseInt(x.toString())));
            put("range", (act_v, exp_v) -> handle(getBlank(act_v), exp_v, (left_y, right_x) -> {
                List<String> y1 = JSONArray.parseArray(JSON.toJSONString(left_y)).toJavaList(String.class);
                if (right_x instanceof List) {
                    //应该走不到
                    List<String> x1 = JSONArray.parseArray(JSON.toJSONString(right_x)).toJavaList(String.class);
                    return org.apache.commons.collections4.CollectionUtils.containsAll(y1, x1);
                } else {
                    return y1.contains(right_x.toString());
                }
            }));
        }

        private Object getBlank(Object answer) {
            if (answer instanceof String) {
                return answer;
            } else if ((answer instanceof List) && !ObjectUtils.isEmpty(answer)) {
                return ((JSONArray) answer).get(0);
            } else {
                return answer;
            }
        }

        private Object getOptions(Object answer) {
            if ((answer instanceof String) || (answer instanceof Integer)) {
                JSONArray ret = new JSONArray();
                ret.add(answer);
                return ret;
            }
            JSONArray options = new JSONArray();
            if (answer instanceof List) {
                ((JSONArray) answer).forEach(ops -> {
                    if (ops instanceof List) {
                        options.add(((JSONArray) ops).get(0));
                    } else if (ops instanceof Integer) {
                        options.add(ops);
                    }
                });
            }
            return options;
        }

        private Boolean handle(Object left_y, Object right_x, BiPredicate<Object, Object> predicate) {
            return predicate.test(right_x, left_y);
        }
    };

    private final ConcurrentHashMap<String, BiFunction<JSONObject, Object, Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray>>> UNIQ_QUESTION_PARSERS = new ConcurrentHashMap<String, BiFunction<JSONObject, Object, Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray>>>() {
        {
            put("blank", this::uniqParseBlank);
            put("multiple_blank", this::uniqParseMultipleBlank);
            put("single", this::uniqParseCommon);
            put("multiple", this::uniqParseMultiple);
            put("score", this::uniqParseCommon);
            put("sort", this::uniqParseCommon);
            put("city", this::uniqParseCommon);
            put("matrix_single", this::uniqParseMatrixSingle);
            put("matrix_multiple", this::uniqParseMatrixMultiple);
            put("q_cascade", this::uniqParseCommon);
            put("q_cascade_blank", this::uniqParseCommon);
            put("matrix_blank", this::uniqParseMatrixBlank);
            put("matrix_score", this::uniqParseMatrixScore);
            put("cascader", this::uniqParseCommon);
            put("timestamp", this::uniqParseCommon);
            put("timedelta", this::uniqParseCommon);
            put("evaluation", this::uniqParseCommon);
            put("geo", this::uniqParseCommon);
            put("auto_table", this::uniqParseAutoTable);
        }

        private Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> uniqParseAutoTable(JSONObject uniq_q, Object ans_data) {
            List<Quartet<Integer, Integer, Integer, Boolean>> ua_tmp_list = new ArrayList<>();
            Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> ua_dict = new HashMap<>();

            int option_id = ObjectUtils.isEmpty(uniq_q.getInteger("op_id")) ? 0 : uniq_q.getInteger("op_id");
            Integer qid = ObjectUtils.isEmpty(uniq_q.getInteger("qid")) ? 0 : uniq_q.getInteger("qid");

            JSONArray option_list;
            if (option_id == 0) {
                option_list = Optional.ofNullable(uniq_q.getJSONArray("option_list")).orElse(new JSONArray());
            } else {
                option_list = Optional.ofNullable(uniq_q.getJSONArray("option_list")).orElseGet(() -> {
                    JSONArray ret = new JSONArray();
                    ret.add(option_id);
                    return ret;
                });
            }

            option_list.forEach((op_id) -> {
                Quartet<Integer, Integer, Integer, Boolean> it = Quartet.with(qid, 0, (Integer) op_id, Boolean.FALSE);
                ua_tmp_list.add(it);
            });

            ua_tmp_list.forEach((ua_cons_data) -> {
                JSONArray check_vals = new JSONArray();
                if (!ObjectUtils.isEmpty(ans_data) && (ans_data instanceof List)) {
                    JSONArray ans_data_arry = (JSONArray) ans_data;
                    ans_data_arry.forEach((row_data) -> {
                        Object ans = ((JSONObject) row_data).get(ua_cons_data.getValue2().toString());
                        Object ret;
                        if (!ObjectUtils.isEmpty(ans) && (ans instanceof List)) {
                            ret = ((JSONArray) ans).get(0);
                        } else {
                            ret = ans;
                        }
                        if (!ObjectUtils.isEmpty(ret)) {
                            check_vals.add(ret);
                        }
                    });
                }
                ua_dict.put(ua_cons_data, check_vals);
            });
            return ua_dict;
        }

        private Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> uniqParseMatrixScore(JSONObject uniq_q, Object ans_data) {
            Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> ua_dict = new HashMap<>();

            Integer qid = ObjectUtils.isEmpty(uniq_q.getInteger("qid")) ? 0 : uniq_q.getInteger("qid");
            JSONArray row_list = Optional.ofNullable(uniq_q.getJSONArray("row_list")).orElse(new JSONArray());

            if (!ObjectUtils.isEmpty(row_list)) {
                row_list.forEach((row_id) -> {
                    JSONArray row_ans = new JSONArray();
                    row_ans.add(((JSONObject) ans_data).get(row_id.toString().concat("_open")));
                    Quartet<Integer, Integer, Integer, Boolean> it = Quartet.with(qid, (Integer) row_id, 0, Boolean.TRUE);
                    ua_dict.put(it, row_ans);
                });
            }
            return ua_dict;
        }

        private Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> uniqParseMatrixBlank(JSONObject uniq_q, Object ans_data) {
            Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> ua_dict = new HashMap<>();

            Integer qid = ObjectUtils.isEmpty(uniq_q.getInteger("qid")) ? 0 : uniq_q.getInteger("qid");
            JSONArray open_list = Optional.ofNullable(uniq_q.getJSONArray("open_list")).orElse(new JSONArray());
            JSONArray row_list = Optional.ofNullable(uniq_q.getJSONArray("row_list")).orElse(new JSONArray());

            if (!ObjectUtils.isEmpty(row_list)) {
                row_list.forEach((row_id) -> {
                    JSONArray row_ans = new JSONArray();
                    row_ans.add(((JSONObject) ans_data).get(row_id.toString().concat("_open")));
                    Quartet<Integer, Integer, Integer, Boolean> it = Quartet.with(qid, (Integer) row_id, 0, Boolean.TRUE);
                    ua_dict.put(it, row_ans);
                });
            }

            if (!ObjectUtils.isEmpty(open_list)) {
                JSONObject ans_data_obj = (JSONObject) ans_data;
                ans_data_obj.forEach((row_id, op_ans_tmp) -> {
                    if (op_ans_tmp instanceof Map) {//if (!(op_ans_tmp instanceof String)) {
                        JSONObject op_ans = (JSONObject) op_ans_tmp;
                        op_ans.forEach((op_id_k, op_value) -> {
                            if (open_list.contains(op_id_k)) {
                                Quartet<Integer, Integer, Integer, Boolean> it = Quartet.with(qid, Integer.valueOf(row_id), Integer.valueOf(op_id_k), Boolean.TRUE);
                                JSONArray row_ans = new JSONArray();
                                row_ans.add(OPEN_PREDIX + op_value.toString());
                                ua_dict.put(it, row_ans);
                            }
                        });
                    }
                });
            }
            return ua_dict;
        }

        private Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> uniqParseMatrixMultiple(JSONObject uniq_q, Object ans_data) {
            Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> ua_dict = new HashMap<>();

            Integer qid = ObjectUtils.isEmpty(uniq_q.getInteger("qid")) ? 0 : uniq_q.getInteger("qid");
            //int option_id = ObjectUtils.isEmpty(uniq_q.getInteger("op_id")) ? 0 : uniq_q.getInteger("op_id");

            //JSONArray option_list;
            //if (option_id == 0) {
            //    option_list = Optional.ofNullable(uniq_q.getJSONArray("option_list")).orElse(new JSONArray());
            //} else {
            //    option_list = Optional.ofNullable(uniq_q.getJSONArray("option_list")).orElseGet(() -> {
            //        JSONArray ret = new JSONArray();
            //        ret.add(option_id);
            //        return ret;
            //    });
            //}
            JSONArray open_list = Optional.ofNullable(uniq_q.getJSONArray("open_list")).orElse(new JSONArray());
            JSONArray row_list = Optional.ofNullable(uniq_q.getJSONArray("row_list")).orElse(new JSONArray());

            if (!ObjectUtils.isEmpty(row_list)) {
                row_list.forEach((row_id) -> {
                    JSONArray row_ans = new JSONArray();
                    row_ans.add(((JSONObject) ans_data).get(row_id.toString().concat("_open")));
                    Quartet<Integer, Integer, Integer, Boolean> it = Quartet.with(qid, (Integer) row_id, 0, Boolean.TRUE);
                    ua_dict.put(it, row_ans);
                });
            }

            if (!ObjectUtils.isEmpty(open_list)) {
                JSONObject ans_data_obj = (JSONObject) ans_data;
                ans_data_obj.forEach((row_id, op_ans_tmp) -> {
                    if (op_ans_tmp instanceof List) {
                        JSONArray op_ans = (JSONArray) op_ans_tmp;
                        op_ans.forEach((tmp) -> {
                            JSONArray ans = (JSONArray) tmp;
                            if (open_list.contains(ans.get(0)) && !ObjectUtils.isEmpty(ans.get(1))) {
                                Quartet<Integer, Integer, Integer, Boolean> it = Quartet.with(qid, Integer.valueOf(row_id), ans.getInteger(0), Boolean.TRUE);
                                JSONArray row_ans = new JSONArray();
                                row_ans.add(OPEN_PREDIX + ans.getString(1));
                                ua_dict.put(it, row_ans);
                            }
                        });
                    }
                });
            }
            return ua_dict;
        }

        private Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> uniqParseMatrixSingle(JSONObject uniq_q, Object ans_data) {
            Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> ua_dict = new HashMap<>();

            Integer qid = ObjectUtils.isEmpty(uniq_q.getInteger("qid")) ? 0 : uniq_q.getInteger("qid");
            //int option_id = ObjectUtils.isEmpty(uniq_q.getInteger("op_id")) ? 0 : uniq_q.getInteger("op_id");

            //JSONArray option_list;
            //if (option_id == 0) {
            //    option_list = Optional.ofNullable(uniq_q.getJSONArray("option_list")).orElse(new JSONArray());
            //} else {
            //    option_list = Optional.ofNullable(uniq_q.getJSONArray("option_list")).orElseGet(() -> {
            //        JSONArray ret = new JSONArray();
            //        ret.add(option_id);
            //        return ret;
            //    });
            //}
            JSONArray open_list = Optional.ofNullable(uniq_q.getJSONArray("open_list")).orElse(new JSONArray());
            JSONArray row_list = Optional.ofNullable(uniq_q.getJSONArray("row_list")).orElse(new JSONArray());

            if (!ObjectUtils.isEmpty(row_list)) {
                row_list.forEach((row_id) -> {
                    JSONArray row_ans = new JSONArray();
                    row_ans.add(((JSONObject) ans_data).get(row_id.toString().concat("_open")));
                    Quartet<Integer, Integer, Integer, Boolean> it = Quartet.with(qid, (Integer) row_id, 0, Boolean.TRUE);
                    ua_dict.put(it, row_ans);
                });
            }

            if (!ObjectUtils.isEmpty(open_list)) {
                JSONObject ans_data_obj = (JSONObject) ans_data;
                ans_data_obj.forEach((row_id, op_ans_tmp) -> {
                    if (op_ans_tmp instanceof List) {
                        JSONArray op_ans = (JSONArray) op_ans_tmp;
                        if (open_list.contains(op_ans.get(0)) && !ObjectUtils.isEmpty(op_ans.get(1))) {
                            Quartet<Integer, Integer, Integer, Boolean> it = Quartet.with(qid, Integer.valueOf(row_id), op_ans.getInteger(0), Boolean.TRUE);
                            JSONArray row_ans = new JSONArray();
                            row_ans.add(OPEN_PREDIX + op_ans.getString(1));
                            ua_dict.put(it, row_ans);
                        }
                    }
                });
            }

            return ua_dict;
        }

        private Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> uniqParseMultiple(JSONObject uniq_q, Object ans_data) {
            List<Quartet<Integer, Integer, Integer, Boolean>> ua_tmp_list = new ArrayList<>();
            Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> ua_dict = new HashMap<>();

            Integer qid = ObjectUtils.isEmpty(uniq_q.getInteger("qid")) ? 0 : uniq_q.getInteger("qid");
            //int option_id = ObjectUtils.isEmpty(uniq_q.getInteger("op_id")) ? 0 : uniq_q.getInteger("op_id");

            //JSONArray option_list;
            //if (option_id == 0) {
            //    option_list = Optional.ofNullable(uniq_q.getJSONArray("option_list")).orElse(new JSONArray());
            //} else {
            //    option_list = Optional.ofNullable(uniq_q.getJSONArray("option_list")).orElseGet(() -> {
            //        JSONArray ret = new JSONArray();
            //        ret.add(option_id);
            //        return ret;
            //    });
            //}
            JSONArray open_list = Optional.ofNullable(uniq_q.getJSONArray("open_list")).orElse(new JSONArray());
            open_list.forEach((op_id) -> {
                Quartet<Integer, Integer, Integer, Boolean> it = Quartet.with(qid, 0, (Integer) op_id, Boolean.TRUE);
                ua_tmp_list.add(it);
            });

            ua_tmp_list.forEach((ua_cons_data) -> {
                JSONArray check_vals = new JSONArray();
                if (!ObjectUtils.isEmpty(ans_data) && (ans_data instanceof List)) {
                    JSONArray ans_data_arry = (JSONArray) ans_data;
                    ans_data_arry.forEach((tmp) -> {
                        JSONArray ans = (JSONArray) tmp;
                        if (ua_cons_data.getValue2().equals(ans.getInteger(0)) && !ObjectUtils.isEmpty(ans.get(1))) {
                            check_vals.add(OPEN_PREDIX + ans.getString(1));
                            ua_dict.put(ua_cons_data, check_vals);
                        }
                    });
                }
            });
            return ua_dict;
        }

        private Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> uniqParseMultipleBlank(JSONObject uniq_q, Object ans_data) {
            Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> ua_dict = new HashMap<>();
            if (!ObjectUtils.isEmpty(ans_data)) {
                Integer qid = ObjectUtils.isEmpty(uniq_q.getInteger("qid")) ? 0 : uniq_q.getInteger("qid");
                JSONArray open_list = Optional.ofNullable(uniq_q.getJSONArray("open_list")).orElse(new JSONArray());
                open_list.forEach((temp) -> {
                    String open_id = (String) temp;
                    JSONArray ans = ((JSONObject) ans_data).getJSONArray(open_id);
                    if (!ObjectUtils.isEmpty(ans)) {
                        Quartet<Integer, Integer, Integer, Boolean> it = Quartet.with(qid, 0, Integer.valueOf(open_id), Boolean.TRUE);
                        JSONArray ret = new JSONArray();
                        ret.add(ans.get(0));
                        ua_dict.put(it, ret);
                    }
                });
            }
            return ua_dict;
        }

        private Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> uniqParseCommon(JSONObject uniq_q, Object ans_data) {
            List<Quartet<Integer, Integer, Integer, Boolean>> ua_tmp_list = new ArrayList<>();
            Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> ua_dict = new HashMap<>();

            int option_id = ObjectUtils.isEmpty(uniq_q.getInteger("op_id")) ? 0 : uniq_q.getInteger("op_id");
            Integer qid = ObjectUtils.isEmpty(uniq_q.getInteger("qid")) ? 0 : uniq_q.getInteger("qid");

            JSONArray option_list;
            if (option_id == 0) {
                option_list = Optional.ofNullable(uniq_q.getJSONArray("option_list")).orElse(new JSONArray());
            } else {
                option_list = Optional.ofNullable(uniq_q.getJSONArray("option_list")).orElseGet(() -> {
                    JSONArray ret = new JSONArray();
                    ret.add(option_id);
                    return ret;
                });
            }

            JSONArray open_list = Optional.ofNullable(uniq_q.getJSONArray("open_list")).orElse(new JSONArray());

            option_list.forEach((op_id) -> {
                Quartet<Integer, Integer, Integer, Boolean> it = Quartet.with(qid, 0, (Integer) op_id, Boolean.FALSE);
                ua_tmp_list.add(it);
            });

            open_list.forEach((op_id) -> {
                Quartet<Integer, Integer, Integer, Boolean> it = Quartet.with(qid, 0, (Integer) op_id, Boolean.TRUE);
                ua_tmp_list.add(it);
            });

            ua_tmp_list.forEach((ua_cons_data) -> {
                JSONArray check_vals = new JSONArray();
                if (!ObjectUtils.isEmpty(ans_data) && (ans_data instanceof List)) {
                    JSONArray ans = (JSONArray) ans_data;
                    if (ua_cons_data.getValue3()) {
                        if (!ObjectUtils.isEmpty(ans.get(1))) {
                            check_vals.add(OPEN_PREDIX + ans.getString(1));
                        }
                    } else {
                        check_vals.add(ans.get(0));
                    }
                    ua_dict.put(ua_cons_data, check_vals);
                }
            });
            return ua_dict;
        }

        private Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> uniqParseBlank(JSONObject uniq_q, Object ans_data) {
            Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> ua_dict = new HashMap<>();
            if (!ObjectUtils.isEmpty(ans_data) && (ans_data instanceof List)) {
                Integer qid = ObjectUtils.isEmpty(uniq_q.getInteger("qid")) ? 0 : uniq_q.getInteger("qid");
                Quartet<Integer, Integer, Integer, Boolean> it = Quartet.with(qid, 0, 0, Boolean.FALSE);
                JSONArray ret = new JSONArray();
                ret.add(((JSONArray) ans_data).get(0));
                ua_dict.put(it, ret);
            }
            return ua_dict;
        }
    };

    private final ConcurrentHashMap<String, BiConsumer<Consumer<JSONObject>, JSONObject>> PROP_DATA_PARSERS = new ConcurrentHashMap<String, BiConsumer<Consumer<JSONObject>, JSONObject>>() {
        {
            put("city", (consumer, dto) -> parseCityToPropData(dto));
            put("geo", (consumer, dto) -> parseGeoToPropData(dto));
            put("single", (consumer, dto) -> parseSingleToPropData(dto));
            put("multiple", (consumer, dto) -> parseMultipleToPropData(dto));
            put("score", (consumer, dto) -> parseScoreToPropData(dto));
            put("proportion", (consumer, dto) -> parseScoreToPropData(dto));
            put("nps", (consumer, dto) -> parseScoreToPropData(dto));
            put("matrix_score", (consumer, dto) -> matrixPropData(this::parseScoreToPropData, dto));
            put("matrix_single", (consumer, dto) -> matrixPropData(this::parseSingleToPropData, dto));
            put("matrix_multiple", (consumer, dto) -> matrixPropData(this::parseMultipleToPropData, dto));
            put("matrix_blank", (consumer, dto) -> matrixPropData(this::parseBlankToPropData, dto));
            put("blank", (consumer, dto) -> parseBlankToPropData(dto));
            put("multiple_blank", (consumer, dto) -> parseBlankToPropData(dto));
            put("cascader", (consumer, dto) -> matrixPropData(this::parseSingleToPropData, dto));
            put("sort", (consumer, dto) -> parseScoreToPropData(dto));
            put("timedelta", (consumer, dto) -> parseBlankToPropData(dto));
            put("timestamp", (consumer, dto) -> parseTimestampToPropData(dto));
            put("evaluation", (consumer, dto) -> parseEvaluationToPropData(dto));
        }

        /**
         * parse_open_answer
         * @author chenglong.yue <chenglong.yue@idiaoyan.com>
         * @date 2023/2/20 14:38
         */
        private String parseOpenAnswer(String answer, JSONArray open_answers) {
            if (answer.length() < 32) {
                return answer;
            }
            String digest = DigestUtil.md5Hex(answer);
            JSONObject item = new JSONObject();
            item.put("digest", digest);
            //survey_openanswer data字段
            //["里程表记录错误，保养几天了，里程表还不到17100公里，可记录了17500多！"]
            item.put("data", Collections.singletonList(answer));
            open_answers.add(item);
            return digest;
        }

        /**
         * gen_prop_data
         * @author chenglong.yue <chenglong.yue@idiaoyan.com>
         * @date 2023/2/20 14:38
         */
        private void genPropData(String gid, String answer, JSONObject q, JSONArray prop_data, JSONArray open_answers, boolean is_value_data) {
            //str = "23_12_14";
            //print str.split('_', 1 );
            //['23', '12_14']

            //String gid = "23_12_14";
            //String[] gid_list = gid.split("_", 2);
            //System.out.println(String.join("->", gid_list));
            String[] gid_list = gid.split("_", 2);
            int gid_list_len = gid_list.length;
            JSONObject item = new JSONObject();
            item.put("qgid", Integer.parseInt(gid_list[0]));
            if (is_value_data) {
                item.put("value", answer);
            } else {
                item.put("open_data", answer);
            }
            if (gid_list_len == 2) {
                item.put("key", gid_list[1]);
            }
            prop_data.add(item);
        }

        /**
         * parse_city_to_prop_data
         * @author chenglong.yue <chenglong.yue@idiaoyan.com>
         * @date 2023/2/20 14:35
         */
        private void parseCityToPropData(JSONObject dto) {
            String gid = dto.getString("gid");
            //{"1":"浙江省","2":"绍兴市","3":"诸暨市"}
            Object answer = dto.get("answer");
            JSONObject q = dto.getJSONObject("q");
            JSONArray prop_data = dto.getJSONArray("prop_data");
            JSONArray open_answers = dto.getJSONArray("open_answers");
            String prev = "";
            List<String> city_list = new ArrayList<>();
            if (answer instanceof Map) {
                String ans;
                for (int idx = 1; idx < 4; idx++) {
                    ans = ((JSONObject) answer).getString(String.valueOf(idx));
                    if (ans == null) {
                        break;
                    }
                    if (StrUtil.equals("", ans)) {
                        ans = prev;
                    }
                    city_list.add(ans);
                    genPropData(gid + "_" + idx, ans, q, prop_data, open_answers, true);
                    prev = ans;
                }
                ans = ((JSONObject) answer).getString("4");
                if (StrUtil.isNotEmpty(ans)) {
                    city_list.add(ans);
                }
                if (!city_list.isEmpty()) {
                    ans = parseOpenAnswer(String.join(",", city_list), open_answers);
                    genPropData(gid, ans, q, prop_data, open_answers, false);
                }
            }
        }


        /**
         * parse_geo_to_prop_data
         * @author chenglong.yue <chenglong.yue@idiaoyan.com>
         * @date 2023/2/20 15:31
         */
        private void parseGeoToPropData(JSONObject dto) {
            //{"city":{"1":"上海市","2":"","3":"黄浦区","4":"南京东路街道"},"coordinate":[121.475494,31.230416],"formattedAddress":"上海市黄浦区南京东路街道上海人民广场"}
            Object answer = dto.get("answer");
            if (answer instanceof List) {
                //老数据格式，只有经纬度
                return;
            }
            Object city = ((JSONObject) answer).get("city");
            dto.put("answer", city);
            parseCityToPropData(dto);
        }

        /**
         * parse_single_to_prop_data
         * @author chenglong.yue <chenglong.yue@idiaoyan.com>
         * @date 2023/2/20 15:32
         */
        private void parseSingleToPropData(JSONObject dto) {
            String gid = dto.getString("gid");
            Object answerObj = dto.get("answer");
            JSONObject q = dto.getJSONObject("q");
            JSONArray prop_data = dto.getJSONArray("prop_data");
            JSONArray open_answers = dto.getJSONArray("open_answers");
            if (answerObj instanceof List) {
                JSONArray answer = (JSONArray) answerObj;
                if (!answer.isEmpty()) {
                    genPropData(gid, answer.getString(0), q, prop_data, open_answers, true);
                    if (answer.size() > 1) {
                        String open_ans = parseOpenAnswer(answer.getString(1), open_answers);
                        genPropData(gid + "_" + answer.getString(0), open_ans, q, prop_data, open_answers, false);
                    }
                }
            }
        }

        /**
         * parse_multiple_to_prop_data
         * @author chenglong.yue <chenglong.yue@idiaoyan.com>
         * @date 2023/2/20 15:42
         */
        private void parseMultipleToPropData(JSONObject dto) {
            String gid = dto.getString("gid");
            Object answerObj = dto.get("answer");
            JSONObject q = dto.getJSONObject("q");
            JSONArray prop_data = dto.getJSONArray("prop_data");
            JSONArray open_answers = dto.getJSONArray("open_answers");
            if (answerObj instanceof List) {
                JSONArray answer = (JSONArray) answerObj;
                for (Object ansobj : answer) {
                    if (ansobj instanceof List) {
                        JSONArray ans = (JSONArray) ansobj;
                        if (!ans.isEmpty()) {
                            genPropData(gid, ans.getString(0), q, prop_data, open_answers, true);
                            if (ans.size() > 1) {
                                String open_ans = parseOpenAnswer(ans.getString(1), open_answers);
                                genPropData(gid + "_" + ans.getString(0), open_ans, q, prop_data, open_answers, false);
                            }
                        }
                    }
                }
            }
        }

        /**
         * parse_score_to_prop_data
         * @author chenglong.yue <chenglong.yue@idiaoyan.com>
         * @date 2023/2/20 15:53
         */
        private void parseScoreToPropData(JSONObject dto) {
            String gid = dto.getString("gid");
            Object answerObj = dto.get("answer");
            JSONObject q = dto.getJSONObject("q");
            JSONArray prop_data = dto.getJSONArray("prop_data");
            JSONArray open_answers = dto.getJSONArray("open_answers");
            if (answerObj instanceof Map) {
                JSONObject answer = (JSONObject) answerObj;
                answer.forEach((op_id, op_val_obj) -> {
                    String score;
                    if (op_val_obj instanceof List) {
                        JSONArray op_val = (JSONArray) op_val_obj;
                        score = op_val.getString(0);
                        if (op_val.size() > 1) {
                            String val = op_val.getString(1);
                            if (StrUtil.isNotBlank(val)) {
                                val = parseOpenAnswer(val, open_answers);
                                genPropData(gid + "_" + op_id, val, q, prop_data, open_answers, false);
                            }
                        }
                    } else if (op_val_obj instanceof Integer) {
                        score = op_val_obj.toString();
                    } else {
                        return;
                    }
                    genPropData(gid + "_" + op_id, score, q, prop_data, open_answers, true);
                });
            }
        }

        /**
         * parse_blank_to_prop_data
         * @author chenglong.yue <chenglong.yue@idiaoyan.com>
         * @date 2023/2/20 16:24
         */
        private void parseBlankToPropData(JSONObject dto) {
            String gid = dto.getString("gid");
            Object answerObj = dto.get("answer");
            JSONObject q = dto.getJSONObject("q");
            JSONArray prop_data = dto.getJSONArray("prop_data");
            JSONArray open_answers = dto.getJSONArray("open_answers");
            if (answerObj instanceof Map) {
                JSONObject answer = (JSONObject) answerObj;
                answer.forEach((op_id, op_val) -> {
                    JSONObject new_dto = new JSONObject();
                    new_dto.putAll(dto);
                    new_dto.put("gid", gid + "_" + op_id);
                    new_dto.put("answer", op_val);
                    parseBlankToPropData(new_dto);
                });
            } else if (answerObj instanceof List) {
                JSONArray answer = (JSONArray) answerObj;
                Object temp = answer.get(0);
                if (temp instanceof Integer) {
                    //时间／日期题为int
                    genPropData(gid, temp.toString(), q, prop_data, open_answers, true);
                } else if ((temp instanceof String) && StrUtil.isNotBlank(temp.toString())) {
                    String open_ans = parseOpenAnswer(temp.toString(), open_answers);
                    genPropData(gid, open_ans, q, prop_data, open_answers, false);
                }
                if (answer.size() > 1 && (StrUtil.isNotBlank(answer.getString(1)) || answer.getIntValue(1) > 0 || answer.getBooleanValue(1))) {
                    String open_ans = parseOpenAnswer(answer.getString(1), open_answers);
                    genPropData(gid, open_ans, q, prop_data, open_answers, false);
                }
            } else if (answerObj instanceof String) {
                String answer = parseOpenAnswer(answerObj.toString(), open_answers);
                genPropData(gid, answer, q, prop_data, open_answers, false);
            } else if (answerObj instanceof Integer) {
                //时间／日期题为int
                genPropData(gid, answerObj.toString(), q, prop_data, open_answers, true);
            }
        }

        /**
         * parse_timestamp_to_prop_data
         * @author chenglong.yue <chenglong.yue@idiaoyan.com>
         * @date 2023/2/20 17:00
         */
        private void parseTimestampToPropData(JSONObject dto) {
            String gid = dto.getString("gid");
            JSONArray answer = dto.getJSONArray("answer");
            JSONObject q = dto.getJSONObject("q");
            JSONArray prop_data = dto.getJSONArray("prop_data");
            JSONArray open_answers = dto.getJSONArray("open_answers");
            genPropData(gid, answer.getString(0), q, prop_data, open_answers, true);
        }

        /**
         * parse_evaluation_to_prop_data
         * @author chenglong.yue <chenglong.yue@idiaoyan.com>
         * @date 2023/2/20 18:01
         */
        private void parseEvaluationToPropData(JSONObject dto) {
            String gid = dto.getString("gid");
            //{"open": "测试", "tags": ["仪表乱"], "score": 2}
            JSONObject answer = dto.getJSONObject("answer");
            JSONObject q = dto.getJSONObject("q");
            JSONArray prop_data = dto.getJSONArray("prop_data");
            JSONArray open_answers = dto.getJSONArray("open_answers");

            String score = answer.getString("score");
            Object tags_obj = answer.get("tags");
            String _open = answer.getString("open");
            JSONArray tags_id = answer.getJSONArray("tagsId");
            //{
            //    "current_template": {
            //        "title": "请您对我们的服务进行评价",
            //        "classify": "service",
            //        "scoreList": [
            //            {
            //                "desc": "非常不满意",
            //                "contentGid": [
            //                    15,
            //                    16,
            //                    17
            //                ]
            //            }
            //        ],
            //        "tagList": {
            //            "15": {
            //                "_id": 1,
            //                "gid": 15,
            //                "oid": 1,
            //                "ooid": [
            //                    "0_0"
            //                ],
            //                "title": "态度冷淡"
            //            },
            //            "16": {
            //                "_id": 2,
            //                "gid": 16,
            //                "oid": 2,
            //                "ooid": [
            //                    "0_1"
            //                ],
            //                "title": "推销多"
            //            }
            //        }
            //    }
            //}
            JSONObject tag_list = Optional.ofNullable(q.getJSONObject("current_template")).map(it -> it.getJSONObject("tagList")).orElse(null);
            genPropData(gid + "_score", score, q, prop_data, open_answers, true);
            if (tags_obj != null) {
                JSONArray tags = new JSONArray();
                if (tags_obj instanceof String) {
                    tags.add(tags_obj);
                }
                for (Object tag : tags) {
                    genPropData(gid + "_tag", tag.toString(), q, prop_data, open_answers, true);
                }
            } else if (!ObjectUtils.isEmpty(tags_id) && !ObjectUtils.isEmpty(tag_list)) {
                for (Object tag_gid : tags_id) {
                    JSONObject tag_obj = tag_list.getJSONObject(tag_gid.toString());
                    if (!ObjectUtils.isEmpty(tag_obj)) {
                        genPropData(gid + "_tag", tag_obj.getString("title"), q, prop_data, open_answers, true);
                    }
                }
            }

            if (StrUtil.isNotBlank(_open)) {
                _open = parseOpenAnswer(_open, open_answers);
                genPropData(gid, _open, q, prop_data, open_answers, false);
            }
        }

        /**
         * _matrix_prop_data
         * @author chenglong.yue <chenglong.yue@idiaoyan.com>
         * @date 2023/2/20 18:21
         */
        private void matrixPropData(Consumer<JSONObject> consumer, JSONObject dto) {
            String gid = dto.getString("gid");
            JSONObject answer = dto.getJSONObject("answer");
            JSONObject q = dto.getJSONObject("q");
            JSONArray prop_data = dto.getJSONArray("prop_data");
            JSONArray open_answers = dto.getJSONArray("open_answers");
            answer.forEach((rid, rv) -> {
                if (rid.endsWith("_open")) {
                    String rid_new = rid.substring(0, rid.indexOf("_open"));
                    String rv_new = parseOpenAnswer(rv.toString(), open_answers);
                    genPropData(gid + "_" + rid_new, rv_new, q, prop_data, open_answers, false);
                } else {
                    JSONObject newdto = new JSONObject();
                    newdto.putAll(dto);
                    newdto.put("gid", gid + "_" + rid);
                    newdto.put("answer", rv);
                    consumer.accept(newdto);
                }
            });
        }
    };

    private final ConcurrentHashMap<String, Integer> event2StatusMap = new ConcurrentHashMap<String, Integer>() {
        {
            put("begin", RespondentStatusEnum.RESPONDENT_STATUS_BEGIN.getValue());
            put("finish", RespondentStatusEnum.RESPONDENT_STATUS_FINISH.getValue());
            put("screen_out", RespondentStatusEnum.RESPONDENT_STATUS_SCREEN_OUT.getValue());
            put("quota_full", RespondentStatusEnum.RESPONDENT_STATUS_QUOTA_FULL.getValue());
            put("system_screen_out", RespondentStatusEnum.RESPONDENT_STATUS_SYSTEM_SCREEN_OUT.getValue());
            put("hidden", RespondentStatusEnum.RESPONDENT_STATUS_HIDDEN.getValue());
            put("backup", RespondentStatusEnum.RESPONDENT_STATUS_BACKUP.getValue());
            put("discard", RespondentStatusEnum.RESPONDENT_STATUS_DISCARD.getValue());
            put("overfull", RespondentStatusEnum.RESPONDENT_STATUS_OVERFULL.getValue());
            put("open", RespondentStatusEnum.RESPONDENT_STATUS_OPEN.getValue());
            put("unfinish", RespondentStatusEnum.RESPONDENT_STATUS_UNFINISH.getValue());
            put("delete", RespondentStatusEnum.RESPONDENT_STATUS_DELETE.getValue());
        }
    };

    @Retryable(value = Exception.class, maxAttempts = 2, backoff = @Backoff(delay = 3600, multiplier = 1.5))
    @Override
    public Boolean testRetryable(Integer code) throws Exception {
        log.info("current-time===============>" + DateUtil.date());
        try {
            NumberUtil.div("3", code.toString());
        } catch (Exception e) {
            if (code > 0) {
                return Boolean.FALSE;
            }
            throw new Exception("手动异常");
        }
        return Boolean.TRUE;
    }

    @Override
    public boolean checkIpExisted(String ip, JSONObject project) {
        log.info("[checkIpExisted],开始执行，project={}", project.toJSONString());
        if (ObjectUtils.isEmpty(project)) {
            return false;
        }
        boolean answer_per_ip;
        JSONObject customAttr = project.getJSONObject("customAttr");
        if (!customAttr.containsKey("ipOnce")) {
            answer_per_ip = false;
        } else {
            answer_per_ip = customAttr.getBooleanValue("ipOnce");
        }
        if (answer_per_ip) {
            if (ObjectUtil.isNotEmpty(customAttr.getJSONArray("except_ips"))
                    && (customAttr.getJSONArray("except_ips").toJavaList(String.class)).contains(ip)) {
                return false;
            }

            return checkIpCount(ip, project);
        }
        return false;
    }

    @Override
    public JSONObject getEnumExtParams(String org_id, Integer status) {
        JSONObject enum_ext_params = new JSONObject();

        JSONObject query = new JSONObject();
        query.put("org_id", org_id);
        query.put("dtype", ExtparamDtypeDataEnum.EXTPARAM_DTYPE_DATA_ENUM.getValue());
        query.put("status", Lists.newArrayList(DELIVEREXTPARAMS_STATUS_START_VALUE));
        List<ExtParamDto> ext_params = openPlatformGrpcClient.listExtParam(query);

        for (ExtParamDto ext_param : ext_params) {
            JSONObject map = new JSONObject();
            map.put("ext_param_id", ext_param.getId());
            JSONObject ext_param_value_map = new JSONObject();
            if (!ObjectUtils.isEmpty(status)) {
                map.put("status", Lists.newArrayList(status));
            }
            List<ExtParamEnumManageDto> ext_param_value_list = openPlatformGrpcClient.listExtParamEnumManage(map);
            for (ExtParamEnumManageDto ext_param_value : ext_param_value_list) {
                ext_param_value_map.put(ext_param_value.getCode(), ext_param_value.getDesc());
            }
            enum_ext_params.put(ext_param.getName(), ext_param_value_map);
        }
        return enum_ext_params;
    }

    @Override
    public List<JSONObject> listExtparam(JSONObject query) {
        List<ExtParamDto> ext_params = openPlatformGrpcClient.listExtParam(query);
        //return JSONObject.parseArray(JSON.toJSONString(ext_params), JSONObject.class);
        //return JSON.parseObject(JSON.toJSONString(ext_params), new TypeReference<List<JSONObject>>() {
        //});
        return ext_params.stream().map(it -> JSONObject.parseObject(JSON.toJSONString(it))).collect(Collectors.toList());
    }

    @Override
    public List<JSONObject> getDeliverExtParams(String deliver_id, String project_id) {
        List<JSONObject> ret;
        Integer survey_ext_version = deliverGrpcClient.getVersionBySurveyId(project_id);
        if (ObjectUtils.isEmpty(survey_ext_version)) {
            throw new BusinessException(String.format("SurveyExtParamsVersion [project_id=%s] not found", project_id));
        }
        String cached_key = deliver_id + "_" + survey_ext_version;
        String deliver_extparams_str = redisService.getString(cached_key);
        if (StrUtil.isNotBlank(deliver_extparams_str)) {
            ret = JSONArray.parseArray(deliver_extparams_str).toJavaList(JSONObject.class);
        } else {
            JSONObject query = new JSONObject();
            query.put("deliver_id", deliver_id);
            query.put("status", DELIVEREXTPARAMS_STATUS_START_VALUE);
            ret = deliverGrpcClient.listDeliverExtparams(query).stream().map(it -> JSONObject.parseObject(JSON.toJSONString(it))).collect(Collectors.toList());
            redisService.set(cached_key, JSON.toJSONString(ret), 24, TimeUnit.HOURS);
        }
        return ret;
    }

    @Override
    public boolean checkQrDeliverDisableV2(Integer source, JSONObject params) {
        String code = params.getString("code");
        String deliver_id = params.getString("deliver_id");
        if (source.equals(RespondentSourceEnum.RESPONDENT_SOURCE_QRCODE.getValue()) && StrUtil.isNotBlank(deliver_id)) {
            if (StrUtil.isNotBlank(code)) {
                JSONObject result = getQrDeliverDisable(deliver_id, code);
                boolean disable = result.getBooleanValue("disable");
                Integer status = result.getInteger("status");
                if (disable) {
                    return false;
                }
                return !status.equals(DEFAULTDELIVER_STATUS_DISACTIVATE_VALUE);
            } else {
                JSONObject result = getQrDeliverDisable(deliver_id, null);
                boolean disable = result.getBooleanValue("disable");
                return !disable;
            }
        }
        return true;
    }

    @Override
    public void cacheWxworkSessionInfo(String session_id, JSONObject data, long timeout) {
        if (!ObjectUtils.isEmpty(data)) {
            String key = WXWORK_MINI_SESSION_PREFIX.concat(":").concat(session_id);
            redisService.set(key, data, timeout, TimeUnit.SECONDS);
        }
    }

    @Override
    public JSONObject getWxworkUserDetail(String session_id) {
        String key = WXWORK_MINI_SESSION_PREFIX.concat(":").concat(session_id);
        Object session = redisService.get(key);
        if (ObjectUtils.isEmpty(session)) {
            throw new BusinessException(String.format("session %s not found", session_id));
        }
        return (JSONObject) session;
    }


    @Override
    public void cacheWxSessionInfo(String session_id, JSONObject data) {
        if (!ObjectUtils.isEmpty(data)) {
            String key = WX_MINI_SESSION_PREFIX.concat(":").concat(session_id);
            redisService.set(key, data);
        }
    }

    @Override
    public JSONObject getWxSessionInfo(String session_id) {
        String key = WX_MINI_SESSION_PREFIX.concat(":").concat(session_id);
        Object session = redisService.get(key);
        if (ObjectUtils.isEmpty(session)) {
            return new JSONObject();
        }
        return (JSONObject) session;
    }


    @Override
    public void checkQrDeliverDisable(Integer source, JSONObject params) {
        String code = params.getString("code");
        String deliver_id = params.getString("deliver_id");
        if (source.equals(RespondentSourceEnum.RESPONDENT_SOURCE_QRCODE.getValue())
                && StrUtil.isNotBlank(deliver_id)) {
            if (StrUtil.isNotBlank(code)) {
                JSONObject result = getQrDeliverDisable(deliver_id, code);
                boolean disable = result.getBooleanValue("disable");
                Integer status = result.getInteger("status");
                if (disable) {
                    log.error("[checkQrDeliverDisable] 该链接已失效1");
                    throw new BusinessException(SurveyErrorCodeEnum.LINK_DISABLE.getValue(), "该链接已失效1");
                }
                if (status.equals(DEFAULTDELIVER_STATUS_DISACTIVATE_VALUE)) {
                    log.error("[checkQrDeliverDisable] 该链接已失效2");
                    throw new BusinessException(SurveyErrorCodeEnum.LINK_DISABLE.getValue(), "该链接已失效2");
                }
            } else {
                JSONObject result = getQrDeliverDisable(deliver_id, null);
                boolean disable = result.getBooleanValue("disable");
                if (disable) {
                    log.error("[checkQrDeliverDisable] 该链接已失效3");
                    throw new BusinessException(SurveyErrorCodeEnum.LINK_DISABLE.getValue(), "该链接已失效3");
                }
            }
        }
    }

    @Override
    public void checkDedicatedDeliverDelete(Integer source, JSONObject params) {
        String ded_code = params.getString("ded_code");
        String deliver_id = params.getString("deliver_id");
        if (source.equals(RespondentSourceEnum.RESPONDENT_SOURCE_DEDICATED.getValue())
                && StrUtil.isNotBlank(deliver_id)) {
            JSONObject result = getDedicatedDeliverDel(deliver_id, ded_code);
            boolean is_deleted = result.getBooleanValue("is_deleted");
            if (is_deleted) {
                log.error("[checkDedicatedDeliverDelete] checkDedicatedDeliverDelete");
                throw new BusinessException(SurveyErrorCodeEnum.LINK_DELETED.getValue(), "该链接已删除");
            }
        }
    }

    @Override
    public Integer checkUniqLink(String project_id, Integer new_seq, JSONObject all_params, Boolean clear_answer) {
        Integer seq = 0;
        if (clear_answer == null) {
            clear_answer = Boolean.FALSE;
        }
        if (all_params == null) {
            all_params = new JSONObject();
        }
        Integer check_submit = NumberUtil.isNumber(all_params.getString("checkSubmit")) ? all_params.getInteger("checkSubmit") : 0;
        String check_code = all_params.getOrDefault("checkCode", "").toString();
        String deliver_g_id = all_params.getOrDefault("deliver_g_id", "").toString();
        String check_ext_code = all_params.getOrDefault("xm_ext_uniq", "").toString();
        String deliver_id = all_params.getOrDefault("deliver_id", "").toString();
        Integer source = NumberUtil.isNumber(all_params.getString("source")) ? all_params.getInteger("source") : 0;
        if (!check_submit.equals(0) && StrUtil.isNotBlank(check_code)) {
            String key = String.format(DLV_CHK_FORMAT, project_id);
            String hkey;
            if (StrUtil.isNotBlank(deliver_g_id)) {
                hkey = deliver_g_id.concat("_").concat(check_code);
            } else if (source.equals(RespondentSourceEnum.RESPONDENT_SOURCE_WECHATMP.getValue())) {
                hkey = deliver_id.concat("_").concat(check_code);
            } else {
                hkey = check_code;
            }
            Object o = redisService.hashGet(key, hkey);

            try {
                seq = Integer.parseInt(o.toString());
                if (seq > 0) {
                    if (clear_answer) {
                        clearAnswersBySeq(project_id, seq);
                    }
                    return seq;
                } else {
                    log.error("该链接已失效1，seq=" + seq);
                    Map<String, Object> mp = new HashMap<>();
                    mp.put("seq", seq);
                    throw new BusinessException(SurveyErrorCodeEnum.DUPLICATE__ANS.getValue(), "该链接已失效1，seq=" + seq, mp);
                }
            } catch (Exception e) {
                log.warn("checkUniqLink error", e);
                if (new_seq != null && new_seq > 0) {
                    seq = new_seq;
                } else {
                    seq = getNextRspdSeq(null);
                }
            }
            log.info("checkUniqLink key{},hkey{},seq{}", key, hkey, seq);
            if (!redisService.hashPutIfAbsent(key, hkey, seq.toString())) {
                //该链接已开始答题
                Object so = redisService.hashGet(key, hkey);
                if (ObjectUtils.isEmpty(so) || Integer.parseInt(so.toString()) < 1) {
                    log.error("该链接已失效2，seq=" + seq);
                    Map<String, Object> mp = new HashMap<>();
                    mp.put("seq", seq);
                    throw new BusinessException(SurveyErrorCodeEnum.DUPLICATE__ANS.getValue(), "该链接已失效2，seq=" + seq, mp);
                }
            }
            return seq;
        } else if (StrUtil.isNotEmpty(check_ext_code)) {
            return checkExtParamsUniqLink(project_id, new_seq, all_params, clear_answer);
        }
        return seq;
    }

    /**
     * 外部参数唯一性链接检查
     *
     * @param project_id   问卷id
     * @param new_seq      seq
     * @param all_params   答题链接参数
     * @param clear_answer 清除答案
     * @return java.lang.Integer
     * @author chenglong.yue <chenglong.yue@idiaoyan.com>
     * @date 2023/2/2 10:06
     */
    private Integer checkExtParamsUniqLink(String project_id, Integer new_seq, JSONObject all_params, Boolean clear_answer) {
        log.error("外部参数唯一性链接检查checkExtParamsUniqLink,project_id={},new_seq={},all_params={},clear_answer={}", project_id, new_seq, all_params.toJSONString(), clear_answer);
        Integer seq = 0;
        if (clear_answer == null) {
            clear_answer = Boolean.FALSE;
        }
        String check_ext_code = all_params.getOrDefault("xm_ext_uniq", "").toString();
        String deliver_id = all_params.getOrDefault("deliver_id", "").toString();

        int cache_seconds = 0;
        if (StrUtil.isNotEmpty(all_params.getString("xm_custom"))) {
            JSONObject query = new JSONObject();
            query.put("deliver_id", deliver_id);
            query.put("survey_id", project_id);
            query.put("param_only_aging_type", CUSTOMLINKDELIVER_PARAM_ONLY_AGING_TYPE_PARAMS_SPECIFIED_VALUE);
            List<CustomLinkDeliverDto> custom_link_deliver_config = deliverGrpcClient.listCustomLinkDeliver(query);
            cache_seconds = custom_link_deliver_config.stream().findFirst().map(CustomLinkDeliverDto::getParamOnlyAgingDays).orElse(0) * 60 * 60 * 24;
        }

        if (StrUtil.isNotEmpty(check_ext_code)) {
            String key = String.format(DLV_EXT_CHK_FORMAT, project_id, check_ext_code);
            Object o = redisService.get(key);

            try {
                seq = Integer.parseInt(o.toString());
                if (seq > 0) {
                    if (clear_answer) {
                        clearAnswersBySeq(project_id, seq);
                    }
                    return seq;
                } else {
                    log.error("该链接已失效1，seq=" + seq);
                    Map<String, Object> mp = new HashMap<>();
                    mp.put("seq", seq);
                    throw new BusinessException(SurveyErrorCodeEnum.DUPLICATE__ANS.getValue(), "该链接已失效1，seq=" + seq, mp);
                }
            } catch (Exception e) {
                log.error("checkExtParamsUniqLink error", e);
                if (new_seq != null && new_seq > 0) {
                    seq = new_seq;
                } else {
                    seq = getNextRspdSeq(null);
                    log.error("checkExtParamsUniqLink getNextRspdSeq seq={}", seq);
                }
            }

            log.error("checkExtParamsUniqLink seq={}", seq);
            boolean ret = redisService.setIfAbsent(key, seq.toString());
            if (ret && cache_seconds > 0) {
                log.error("如果 外部参数唯一性校验开启 时间限制，key 设置过期时间");
                // 如果 外部参数唯一性校验开启 时间限制，key 设置过期时间
                redisService.expire(key, cache_seconds, TimeUnit.SECONDS);
            }

            if (!ret) {
                //该链接已开始答题
                Object o1 = redisService.get(key);
                seq = Integer.parseInt(o1.toString());
                if (seq == 0) {
                    // hkey的值在提交的时候会被设置为0
                    log.error("外部参数唯一性， 校验失败" + seq);
                    throw new BusinessException(SurveyErrorCodeEnum.EXT_PARAMS_UNIQUE_CHECK_FAIL.getValue(), "外部参数唯一性， 校验失败");
                }
            }
        }
        return seq;
    }

    @Override
    public Integer getNextRspdSeq(Integer count) {
        long init_step = 100000;
        if (count == null) {
            count = 1;
        }
        Integer seq = serviceSequenceService.getSequence(RSPD_SEQ_REDIS_KEY, count);
        if (seq >= init_step) {
            return seq;
        }
        seq = serviceSequenceService.getSequence(RSPD_SEQ_REDIS_KEY, init_step);
        if (count > 1) {
            return serviceSequenceService.getSequence(RSPD_SEQ_REDIS_KEY, count);
        }
        return seq;
    }

    @Override
    public void clearAnswersBySeq(String projectId, Integer seq) {
        log.error("clearAnswersBySeq projectId={},seq={}", projectId, seq);
        String name = String.format(RSPD_FORMAT, seq);
        List<String> answer_keys = redisService.hashKeys(name).stream()
                .map(Object::toString)
                .filter(NumberUtil::isNumber)
                .collect(Collectors.toList());
        /*Set<String> keys = redisService.hashKeys(name).stream().map(Object::toString).collect(Collectors.toSet());
        List<String> answer_keys = new ArrayList<>();
        for (String key : keys) {
            if (NumberUtil.isNumber(key)) {
                answer_keys.add(key);
            }
        }*/
        log.error("clearAnswersBySeq answer_keys={}", answer_keys);
        if (!CollectionUtils.isEmpty(answer_keys)) {
            redisService.hashDel(name, answer_keys);

            surveyRespondentDataMapper.update(null,
                    new UpdateWrapper<SurveyRespondentData>().lambda()
                            .eq(SurveyRespondentData::getProjectId, projectId)
                            .eq(SurveyRespondentData::getSeq, seq)
                            .set(SurveyRespondentData::getAnswers, "{}"));

            clearAttachments(projectId, seq);
        }
    }

    private void clearAttachments(String projectId, Integer seq) {
        List<SurveyRespondentAttachment> list = surveyRespondentAttachmentMapper.selectList(new QueryWrapper<SurveyRespondentAttachment>().lambda()
                .eq(SurveyRespondentAttachment::getProjectId, projectId)
                .eq(SurveyRespondentAttachment::getSeq, seq));

        list.forEach(att -> deleteAttachment(projectId, seq, att.getId()));
    }

    @Override
    public Integer deleteAttachment(String pid, Integer seq, String att_id) {
        SurveyRespondentAttachment att = surveyRespondentAttachmentMapper.selectOne(new QueryWrapper<SurveyRespondentAttachment>().lambda()
                .eq(SurveyRespondentAttachment::getProjectId, pid)
                .eq(SurveyRespondentAttachment::getId, att_id)
                .eq(SurveyRespondentAttachment::getSeq, seq)
                .last("limit 1"));
        if (!ObjectUtils.isEmpty(att)) {
            surveyRespondentAttachmentMapper.deleteById(att_id);
        } else {
            throw new BusinessException(String.format("attachment[id=%s,project_id=%s,seq=%s] not found", att_id, pid, seq));
        }
        return 1;
    }

    @Override
    public List<SurveyColumnVo> getColumnsNeDelete(String projectId, JSONObject args, String orgId) {
        LambdaQueryWrapper<SurveyColumn> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SurveyColumn::getProjectId, projectId)
                .ne(SurveyColumn::getStatus, COLUMN_STATUS_DELETE.getNumber());
        if (ObjectUtil.isNotEmpty(args.getJSONArray("ctype"))) {
            queryWrapper.in(SurveyColumn::getCtype, args.getJSONArray("ctype"));
        }
        if (ObjectUtil.isNotEmpty(args.getJSONArray("qtype"))) {
            queryWrapper.in(SurveyColumn::getQtype, args.getJSONArray("qtype"));
        }
        if (ObjectUtil.isNotEmpty(args.getString("gid"))) {
            queryWrapper.eq(SurveyColumn::getGid, args.getString("gid"));
        }
        //queryWrapper.orderByAsc(SurveyColumn::getCtype, SurveyColumn::getSortNo);
        queryWrapper.orderByAsc(SurveyColumn::getSortNo);
        queryWrapper.orderByAsc(SurveyColumn::getCtype);
        JSONObject query = new JSONObject();
        query.put("org_id", orgId);
        query.put("status", Collections.singletonList(DELIVEREXTPARAMS_STATUS_START.getNumber()));
        List<JSONObject> org_ext_param = listExtparam(query);
        Map<String, String> ext_params_desc_map = new HashMap<>();
        for (JSONObject o : org_ext_param) {
            ext_params_desc_map.put(o.getString("name"), o.getString("desc"));
        }

        List<SurveyColumn> cursor = surveyColumnMapper.selectList(queryWrapper);
        List<SurveyColumnVo> ctype_columns_list;
        HashMap<Object, List<SurveyColumnVo>> ctype_columns = new HashMap<>();
        List<SurveyColumnVo> newList = new ArrayList<>();
        for (SurveyColumn column : cursor) {
            SurveyColumnVo surveyColumnVo = surveyConvertMapper.surveyColumn2Vo(column);
            if (column.getCtype() == COLUMN_CTYPE_EXTERNAL.getNumber() && ObjectUtil.isNotEmpty(ext_params_desc_map)) {
                surveyColumnVo.setDesc(ext_params_desc_map.get(column.getCol()));
                if (ObjectUtil.isNotEmpty(surveyColumnVo.getDesc())) {
                    surveyColumnVo.setColDesc(surveyColumnVo.getDesc() + "|" + column.getCol());
                } else {
                    surveyColumnVo.setColDesc(column.getCol());
                }
            } else if (column.getCtype() == COLUMN_CTYPE_EXTERNAL.getNumber()) {
                surveyColumnVo.setColDesc(column.getCol());
            }
            ctype_columns_list = ctype_columns.getOrDefault(column.getCtype(), new ArrayList<>());
            ctype_columns_list.add(surveyColumnVo);
            ctype_columns.put(column.getCtype(), ctype_columns_list);
        }
        for (Integer tmp : COLUMNS_META_SORT) {
            if (ctype_columns.containsKey(tmp)) {
                newList.addAll(ctype_columns.get(tmp));
            }
        }
        return newList;
    }

    /**
     * 判断投放答题时能 是否可以答题
     */
    @Override
    public void checkDeliverEnableRespondent(String deliver_id) {
        String key = String.format(DELIVER_DELIVER_ENABLE_RESPONDENT_FORMAT, deliver_id);
        Object enable_respondent = redisService.get(key);
        if (!ObjectUtils.isEmpty(enable_respondent)) {
            if (StrUtil.equals("0", enable_respondent.toString())) {
                //判断值是否为“1” 为“1”可以答题
                log.error("问卷投放不容许答题1");
                throw new BusinessException(SurveyErrorCodeEnum.SURVEY_FINISHED.getValue(), "问卷投放不容许答题1");
            }
        }

        //如果没有值则查询deliver
        DeliverDto deliver = deliverGrpcClient.getDeliver(deliver_id);
        if (Objects.isNull(deliver)) {
            log.error(deliver_id + "没有对应的投放");
            throw new BusinessException(SurveyErrorCodeEnum.SURVEY_FINISHED.getValue(), deliver_id + "没有对应的投放");
        }

        if (deliver.getEnableRespondent() == null || deliver.getEnableRespondent().equals(0)) {
            log.error("问卷投放不容许答题2");
            //设置redis 值为”0“ 不容许答题
            redisService.set(key, "0");
            throw new BusinessException(SurveyErrorCodeEnum.SURVEY_FINISHED.getValue(), deliver_id + "问卷投放不容许答题2");
        } else {
            //设置redis 值为”1“ 容许答题
            redisService.set(key, "1");
        }
    }

    @Override
    public ProjectDto post4SurvyCommon(Integer source, String project_id, HttpServletRequest request, String group_code) {
        ProjectDto project = surveyGrpcConvertMapper.toProjectDto(qdesGrpcClient.getProject(project_id));
        if (project.getStatus().equals(QDES_PROJECT_STATUS_RELEASED)) {
            if (!source.equals(RESPONDENT_SOURCE_PREVIEW_VALUE)) {
                JSONObject attr = JSONObject.parseObject(project.getCustomAttr());
                if (attr.getBooleanValue("time_flag")) {
                    String begin_time = attr.getString("begin_time");
                    //if begin_time and time.time() < (int(begin_time) // 1000):
                    //time.time()返回秒
                    //currentTimeMillis 返回毫秒
                    if (StrUtil.isNotBlank(begin_time) && System.currentTimeMillis() < Long.parseLong(begin_time)) {
                        log.error("[post4SurvyCommon]问卷未开始,begin_time={}", begin_time);
                        Map<String, Object> map = new HashMap<>();
                        map.put("begin_time", Long.parseLong(begin_time));
                        throw new BusinessException(SurveyErrorCodeEnum.SURVEY_NOT_BEGIN.getValue(), "问卷未开始", map);
                    }
                }
                //限额检查不需要
                //检查有效样本数
                if (attr.getBooleanValue("nums_flag") && StrUtil.isNotBlank(attr.getString("sample_num"))) {
                    //通过维护redis 计数器获取答卷数量
                    String key = String.format(SURVEY_PROJECT_ID_CNT_RESPONDENT_FORMAT, project_id);
                    int count = Objects.isNull(redisService.get(key)) ? 0 : (Integer) redisService.get(key);
                    if (count >= attr.getInteger("sample_num")) {
                        try {
                            //达到有效样本数后结束问卷
                            finishProject(project_id);
                        } catch (Exception e) {
                            log.error("grpc:finishProject(" + project_id + ") failed");
                        }
                        log.error("[post4SurvyCommon]问卷达到有效样本数后自动结束问卷");
                        throw new BusinessException(SurveyErrorCodeEnum.SURVEY_FINISHED.getValue(), "问卷已结束");
                    }
                }
            }
        } else if (project.getStatus().equals(QDES_PROJECT_STATUS_CREATED)) {
            if (!source.equals(RESPONDENT_SOURCE_PREVIEW_VALUE)) {
                log.error("[post4SurvyCommon]问卷未发布");
                throw new BusinessException(SurveyErrorCodeEnum.SURVEY_NOT_RELEASED.getValue(), "问卷未发布");
            }
        } else if (project.getStatus().equals(QDES_PROJECT_STATUS_FINISHED)) {
            if (!source.equals(RESPONDENT_SOURCE_PREVIEW_VALUE)) {
                log.error("[post4SurvyCommon]问卷已结束");
                throw new BusinessException(SurveyErrorCodeEnum.SURVEY_FINISHED.getValue(), "问卷已结束");
            }
        } else if (project.getStatus().equals(QDES_PROJECT_STATUS_DELETED)) {
            log.error("[post4SurvyCommon]问卷已删除");
            throw new BusinessException(SurveyErrorCodeEnum.SURVEY_DELETED.getValue(), "问卷已删除");
        }

        ipCheck(project, source, request);
        //python直接返回了，照搬，管他妈的啥原因
        //checkPersonIdIsAnswered(project_id);
        if (StrUtil.isNotBlank(group_code)) {
            Organization org = userGrpcClient.getByIdOrCode(project.getCompanyId(), group_code);
            if (ObjectUtils.isEmpty(org)) {
                log.error("[post4SurvyCommon]门店已删除");
                throw new BusinessException(SurveyErrorCodeEnum.GROUP_DELETED.getValue(), "门店已删除");
            }
        }
        return project;
    }

    //private void checkPersonIdIsAnswered(String projectId) {
    //}

    private void ipCheck(ProjectDto projectDto, Integer source, HttpServletRequest request) {
        //一步就转成JSONObject，json类型的字段也不会转义
        //ObjectMapper objectMapper = new ObjectMapper();
        //objectMapper.registerModule(new JavaTimeModule());
        JSONObject proj;
        try {
            String json = objectMapper.writeValueAsString(projectDto);
            proj = JSONObject.parseObject(json);
            log.info("[ipCheck],开始执行，proj={}", proj);
        } catch (JsonProcessingException e) {
            log.error("[ipCheck],JsonProcessingException");
            throw new BusinessException("[ipCheck],JsonProcessingException");
        }

        if (!source.equals(RESPONDENT_SOURCE_PREVIEW_VALUE) && checkIpExisted(ServletUtil.getClientIP(request), proj)) {
            log.error("[ipCheck],您所在的IP已经参与过该调查");
            throw new BusinessException(SurveyErrorCodeEnum.IP_CHECK_ERROR.getValue(), "您所在的IP已经参与过该调查");
        }
    }

    @Override
    public String getCorpUrl() {
        //私有化写死即可
        return surveyDomain;
    }

    @Override
    public String getDeliverSecret() {
        return deliverSecret;
    }

    @Override
    public String getSurveySecret() {
        return surveySecret;
    }

    @Override
    public void projectVersionCheck(ProjectDto project, Integer version) {
        if (!version.equals(project.getReleaseVer())) {
            log.error("[projectVersionCheck]");
            throw new BusinessException(SurveyErrorCodeEnum.PROJECT_VERSION_NOT_MATCH_ERROR.getValue(), "很抱歉，在您答题过程中，我们更新了问卷。为了保证答题的顺利进行，您需要重新开始答题。给您带来的不便，我们深表歉意。");
        }
    }

    @Override
    public void projectVersionCheckPreview(ProjectDto project, Integer version) {
        if (!version.equals(project.getVersion())) {
            log.error("[projectVersionCheckPreview]");
            throw new BusinessException(SurveyErrorCodeEnum.PROJECT_VERSION_NOT_MATCH_ERROR.getValue(), "很抱歉，在您答题过程中，我们更新了问卷。为了保证答题的顺利进行，您需要重新开始答题。给您带来的不便，我们深表歉意。");
        }
    }

    @Override
    public void queryTimeV2(ProjectDto project, SurveyRespondent rspd, Integer seq) {
        if (ObjectUtils.isEmpty(project) || seq == null || seq == 0) {
            return;
        }
        LocalDateTime begin_time;
        if (rspd != null) {
            //如果该答卷的状态为10（未完成）
            if (rspd.getStatus().equals(RESPONDENT_STATUS_UNFINISH_VALUE)) {
                Map<String, Object> mp = new HashMap<>();
                mp.put("seq", seq);
                throw new BusinessException(SurveyErrorCodeEnum.EXCEED_ANS_TIME.getValue(), String.format("未完成答卷无法继续答题,seq=%s", seq), mp);
            }
            //获取Respondent表中begin_time的值
            begin_time = rspd.getBeginTime();
        } else {
            begin_time = getRspdCreated(seq);
        }
        if (begin_time == null) {
            return;
        }
        // 获取设置参数
        JSONObject custom_attr = JSONObject.parseObject(project.getCustomAttr());
        if (!custom_attr.getBooleanValue("switch_limit")) {
            return;
        }
        if (custom_attr.getIntValue("limit_time") == 0) {
            return;
        }
        mergeCode(begin_time, custom_attr.getIntValue("limit_time"), seq, project.getId());
    }

    private void mergeCode(LocalDateTime begin_time, int limit_time, Integer seq, String project_id) {
        //计算limit_time小时前
        //end_time = datetime.datetime.now() - datetime.timedelta(hours=limit_time)
        LocalDateTime end_time = LocalDateTime.now().minusHours(limit_time);
        if (end_time.isAfter(begin_time)) {
            //已过期，将Respondent表中进行中的答卷状态修改成未完成
            LambdaQueryWrapper<SurveyRespondent> queryWrapper = Wrappers.lambdaQuery();
            queryWrapper.eq(SurveyRespondent::getProjectId, project_id);
            queryWrapper.eq(SurveyRespondent::getSeq, seq);
            queryWrapper.eq(SurveyRespondent::getStatus, RESPONDENT_STATUS_BEGIN_VALUE);
            queryWrapper.last("limit 1");
            SurveyRespondent rspd = surveyRespondentMapper.selectOne(queryWrapper);

            if (!ObjectUtils.isEmpty(rspd)) {
                rspd.setStatus(RESPONDENT_STATUS_UNFINISH_VALUE);
                surveyRespondentMapper.update(null, new LambdaUpdateWrapper<SurveyRespondent>().
                        eq(SurveyRespondent::getProjectId, project_id)
                        .eq(SurveyRespondent::getSeq, seq)
                        .eq(SurveyRespondent::getStatus, RESPONDENT_STATUS_BEGIN_VALUE)
                        .set(SurveyRespondent::getStatus, RESPONDENT_STATUS_UNFINISH_VALUE));
                //无返回值异步任务
                CompletableFuture.runAsync(() -> saveRspdPropTaskWrapper(project_id, seq, rspd, null, null, null, null, null), saveRspdPropTaskExecutor);
                //发送mq
                //await redis_mq.publish(topics.survey_respondent_unfinish, {"seq": rspd.seq,
                //        "deliver_group_id": rspd.deliver_group_id,
                //        "deliver_id": rspd.deliver_id,
                //        "member_id": rspd.member_id,
                //        "project_id": project_id,
                //        "dlvr_rid": rspd.dlvr_rid})

                JSONObject mq_data = new JSONObject();
                mq_data.put("seq", rspd.getSeq());
                mq_data.put("deliver_group_id", rspd.getDeliverGroupId());
                mq_data.put("deliver_id", rspd.getDeliverId());
                mq_data.put("member_id", rspd.getMemberId());
                mq_data.put("project_id", project_id);
                mq_data.put("dlvr_rid", rspd.getDeliverId());
                surveyMessageSendService.surveyRespondentUnfinishSend(mq_data);

            }

            //同时将redis中的缓存删除掉
            log.error("mergeCode redisDataCleanBySeq");
            redisDataCleanBySeq(project_id, seq);
            Map<String, Object> mp = new HashMap<>();
            mp.put("seq", seq);
            throw new BusinessException(SurveyErrorCodeEnum.EXCEED_ANS_TIME.getValue(), String.format("该答卷已超出答题时限,seq=%s", seq), mp);
        }
    }

    @Override
    public void redisDataCleanBySeq(String project_id, Integer seq) {
        String key1 = String.format(RSPD_FORMAT, seq);
        String key2 = String.format(Q_PATH_FORMAT, seq);
        String key3 = String.format(CREATED_FORMAT, seq);

        redisService.setRemove(String.format("seq_%s", project_id), seq);
        redisService.del(Lists.newArrayList(key1, key2, key3));

        redisService.expire(String.format(RSPD_TIME_FORMAT, seq), 30, TimeUnit.DAYS);
        redisService.expire(String.format(RSPD_STATUS_FORMAT, seq), 30, TimeUnit.DAYS);
    }

    @Override
    public void saveRspdPropTaskWrapper(String project_id, Integer seq, SurveyRespondent rspd, JSONObject answers, JSONObject others, JSONObject other_open, Integer status, LocalDateTime begin_time) {
        if (!ObjectUtils.isEmpty(rspd)) {
            begin_time = rspd.getBeginTime();
        } else if (!ObjectUtils.isEmpty(begin_time)) {
            log.info("like python pass");
        } else {
            rspd = surveyRespondentMapper.selectOne(new LambdaQueryWrapper<SurveyRespondent>()
                    .eq(SurveyRespondent::getProjectId, project_id)
                    .eq(SurveyRespondent::getSeq, seq)
                    .last("limit 1"));
            begin_time = !ObjectUtils.isEmpty(rspd) ? rspd.getBeginTime() : null;
        }

        if (Objects.isNull(answers)) {
            if (Objects.isNull(others)) {
                others = new JSONObject();
            }
            if (Objects.isNull(other_open)) {
                other_open = new JSONObject();
            }
            SurveyRespondentData rspd_data = surveyRespondentDataMapper.selectOne(new LambdaQueryWrapper<SurveyRespondentData>()
                    .eq(SurveyRespondentData::getProjectId, project_id)
                    .eq(SurveyRespondentData::getSeq, seq)
                    .last("limit 1"));
            if (Objects.isNull(rspd_data)) {
                return;
            }
            genOtherProps(rspd, rspd_data, others, other_open);
            answers = JSONObject.parseObject(rspd_data.getAnswers());
        }

        saveRspdProp(project_id, seq, answers, others, other_open, begin_time, status);
    }

    private void genOtherProps(SurveyRespondent rspd, SurveyRespondentData rspd_data, JSONObject others, JSONObject other_open) {
        if (rspd == null) {
            return;
        }
        others.put("version", rspd.getVersion());
        others.put("group_id", rspd.getGroupId());
        others.put("source", rspd.getSource());
        others.put("status", rspd.getStatus());
        if (StrUtil.isNotBlank(rspd.getDeliverId())) {
            others.put("deliver_id", rspd.getDeliverId());
        }
        if (StrUtil.isNotBlank(rspd.getDeliverGroupId())) {
            others.put("deliver_group_id", rspd.getDeliverGroupId());
        }
        if (StrUtil.isNotBlank(rspd.getMemberId())) {
            others.put("member_id", rspd.getMemberId());
        }
        if (StrUtil.isNotBlank(rspd_data.getCustomAnswers()) && JSONUtil.isTypeJSONObject(rspd_data.getCustomAnswers())) {
            JSONObject c = JSONObject.parseObject(rspd_data.getCustomAnswers());
            if (!CollectionUtils.isEmpty(c)) {
                c.forEach((k, v) -> other_open.put("custom_data__" + k, v));
            }
        }
    }

    @Override
    public void saveRspdProp(String project_id, Integer seq, JSONObject answers, JSONObject others, JSONObject other_open, LocalDateTime begin_time, Integer status) {
        Integer version = null;
        if (!ObjectUtils.isEmpty(others)) {
            //过滤预览答卷，预览答卷不需要筛选
            if (others.getIntValue("source") == RESPONDENT_SOURCE_PREVIEW_VALUE) {
                return;
            }
            version = others.getInteger("version");
        }

        List<QdesQuestionDto> qlist = qdesGrpcClient.getReleasedProjectQuestions(project_id, version, null);
        JSONObject qdict = new JSONObject();
        qlist.forEach(q -> qdict.put(q.getGid().toString(), JSONObject.parseObject(JSONObject.toJSONString(q))));

        JSONArray props = new JSONArray();
        JSONArray open_answers = new JSONArray();
        //这个变量，后面没用
        JSONArray qid_list = new JSONArray();
        answers.forEach((qid, qv) -> {
            JSONObject q = qdict.getJSONObject(qid);
            if (!ObjectUtils.isEmpty(q)) {
                qid_list.add(qid);
                String qtype = q.getString("qtype");
                if (StrUtil.isNotBlank(qtype) && PROP_DATA_PARSERS.containsKey(qtype)) {
                    JSONObject dto = new JSONObject();
                    dto.put("gid", qid);
                    dto.put("answer", qv);
                    dto.put("q", q);
                    dto.put("prop_data", props);
                    dto.put("open_answers", open_answers);
                    PROP_DATA_PARSERS.get(qtype).accept(null, dto);
                }
            }
        });

        //这个变量，后面没用
        JSONObject prop_params = new JSONObject();
        JSONObject other_props = new JSONObject();

        if (!ObjectUtils.isEmpty(others)) {
            others.remove("deliver_id");
            others.forEach((key, val) -> {
                if (val instanceof List) {
                    for (Object v : (List<?>) val) {
                        other_props.put(key, v.toString());
                    }
                } else {
                    other_props.put(key, val.toString());
                }
            });
        }

        if (status != null && status.equals(RESPONDENT_STATUS_DISCARD_VALUE)) {
            other_props.put("status", status);
        }

        if (!ObjectUtils.isEmpty(other_open)) {
            other_open.forEach((key, val) -> {
                if (val instanceof List) {
                    for (Object v : (List<?>) val) {
                        JSONObject d = new JSONObject();
                        d.put("key", key);
                        d.put("open_data", v.toString());
                        props.add(d);
                    }
                } else {
                    JSONObject d = new JSONObject();
                    d.put("key", key);
                    d.put("open_data", val.toString());
                    props.add(d);
                }
            });
        }

        //这一坨看下来没吊用，没接受返回值，也没update字段，
        //if open_answers:
        //    for open_answer in open_answers:
        //        query = params_to_query(OpenAnswer, open_answer[0])
        //        await openanswer_dao.get_one_and_update(None, _query=query)
        prop_params.put("project_id", project_id);
        if (begin_time != null) {
            prop_params.put("begin_time", begin_time);
        }

        //删除之前存储在respondentpropdata 里面的数据
        surveyRespondentPropDataMapper.delete(new LambdaQueryWrapper<SurveyRespondentPropData>()
                .eq(SurveyRespondentPropData::getSeq, seq)
                .eq(SurveyRespondentPropData::getProjectId, project_id));
        List<JSONObject> create_list = new ArrayList<>();
        if (!props.isEmpty()) {
            for (Object prop : props) {
                JSONObject tmp = new JSONObject();
                tmp.put("project_id", project_id);
                tmp.put("seq", seq);
                tmp.put("rspd_prop_id", new ObjectId().toHexString());

                tmp.putAll(other_props);
                tmp.putAll((JSONObject) prop);
                tmp.remove("member_id");
                create_list.add(tmp);
            }
        }

        if (!create_list.isEmpty()) {
            List<SurveyRespondentPropData> list = create_list.stream().map(itme -> itme.toJavaObject(SurveyRespondentPropData.class)).collect(Collectors.toList());
            List<List<SurveyRespondentPropData>> spit_list = ListUtil.split(list, 10);
            for (List<SurveyRespondentPropData> l : spit_list) {
                surveyRespondentPropDataMapper.insertBatchSomeColumn(l);
            }
        }
    }

    @Override
    public LocalDateTime getRspdCreated(Integer seq) {
        String key = String.format(RSPD_FORMAT, seq);
        Object t = redisService.hashGet(key, "begin_time");
        if (!ObjectUtils.isEmpty(t)) {
            return LocalDateTimeUtil.of(Long.parseLong(t.toString()) * 1000);
        }
        //python TODO 兼容旧答卷，下次迭代可删除
        String key1 = String.format(CREATED_FORMAT, seq);
        Object t1 = redisService.get(key1);
        if (ObjectUtils.isEmpty(t1)) {
            return null;
        }
        redisService.hashSet(key, "begin_time", t1);
        return LocalDateTimeUtil.of(Long.parseLong(t1.toString()) * 1000);
    }

    @Override
    public JSONObject decryptExtparamters(JSONObject params, ProjectDto project) {
        if (!ObjectUtils.isEmpty(params)) {
            SurveyExtParameter ext_param = surveyExtParameterService.getExtParam(project.getId(), project.getReleaseVer().toString(), null);
            if (ext_param != null && StrUtil.isAllNotEmpty(ext_param.getAesKey(), ext_param.getData())) {
                if (JSONUtil.isTypeJSONArray(ext_param.getData())) {
                    JSONArray data = JSONArray.parseArray(ext_param.getData());
                    if (!data.isEmpty()) {
                        for (Object it : data) {
                            JSONObject ext = (JSONObject) it;
                            Object val = params.get(ext.getString("name"));
                            if (ext.getBooleanValue("aes_encrypt") && !ObjectUtils.isEmpty(val) && val instanceof String) {
                                int dtype = params.getIntValue(ext.getString("dtype"));
                                String valstr = SignUtil.decryptFromString(ext_param.getAesKey(), val.toString(), Mode.ECB, Padding.ZeroPadding);
                                if (dtype == EXTPARAMTER_DTYPE_INT) {
                                    val = Integer.parseInt(valstr);
                                } else if (dtype == EXTPARAMTER_DTYPE_BOOL) {
                                    if (Lists.newArrayList("true", "1").contains(val.toString().toLowerCase())) {
                                        val = Boolean.TRUE;
                                    } else if (Lists.newArrayList("false", "0").contains(val.toString().toLowerCase())) {
                                        val = Boolean.FALSE;
                                    }
                                }
                                params.put(ext.getString("name"), val);
                            }
                        }
                    }
                }
            }
        }
        return params;
    }

    @Override
    public void checkExtParamsUniqueness(String deliver_id, String project_id, Integer seq, JSONObject custom_answers, int xm_custom) {
        log.error("checkExtParamsUniqueness  deliver_id={},  project_id={},  seq={},  custom_answers={},  xm_custom={}", deliver_id, project_id, seq, custom_answers.toJSONString(), xm_custom);
        if (StrUtil.isNotBlank(deliver_id) && xm_custom > 0) {
            List<String> ext_param_ls = getDeliverExtParams(deliver_id, project_id).stream()
                    .filter(it -> it.getBooleanValue("survey_unique"))
                    .map(obj -> obj.getString("extparam_name"))
                    .collect(Collectors.toList());

            if (!CollectionUtils.isEmpty(ext_param_ls)) {
                JSONObject query = new JSONObject();
                query.put("deliver_id", deliver_id);
                query.put("survey_id", project_id);
                List<CustomLinkDeliverDto> custom_link_deliver_config = deliverGrpcClient.listCustomLinkDeliver(query);

                QueryWrapper<SurveyDeliverUniqueAnswer> queryWrapper;
                if (!CollectionUtils.isEmpty(custom_link_deliver_config)) {
                    queryWrapper = new QueryWrapper<>();
                    queryWrapper.select("seq,count(*) as count");
                    queryWrapper.groupBy("seq");
                    queryWrapper.having("count >= {0}", ext_param_ls.size());

                    Integer param_only_aging_type = custom_link_deliver_config.stream().findFirst().map(CustomLinkDeliverDto::getParamOnlyAgingType).orElse(0);
                    if (param_only_aging_type == CUSTOMLINKDELIVER_PARAM_ONLY_AGING_TYPE_PARAMS_PERMANENT_VALUE) {
                        ext_param_ls.forEach(name -> {
                            queryWrapper.eq("deliver_id", deliver_id);
                            queryWrapper.eq("ext_param_name", name);
                            queryWrapper.eq("ext_param_val", custom_answers.getString(name));
                            queryWrapper.or();
                        });

                    } else {
                        Integer param_only_aging_days = custom_link_deliver_config.stream().findFirst().map(CustomLinkDeliverDto::getParamOnlyAgingDays).orElse(0);
                        LocalDateTime limit_time = LocalDateTime.now().plusDays(param_only_aging_days);
                        ext_param_ls.forEach(name -> {
                            queryWrapper.eq("deliver_id", deliver_id);
                            queryWrapper.eq("ext_param_name", name);
                            queryWrapper.eq("ext_param_val", custom_answers.getString(name));
                            queryWrapper.apply("date_format(created_dt,'%Y-%m-%d %H:%i:%S') <= date_format({0},'%Y-%m-%d %H:%i:%S')", LocalDateTimeUtil.formatNormal(limit_time));
                            queryWrapper.or();
                        });
                    }
                } else {
                    queryWrapper = null;
                }

                if (queryWrapper != null) {
                    List<Map<String, Object>> ret = surveyDeliverUniqueAnswerMapper.selectMaps(queryWrapper);
                    if (!CollectionUtils.isEmpty(ret)) {
                        log.error("checkExtParamsUniqueness 外部参数唯一性， 校验失败");
                        throw new BusinessException(SurveyErrorCodeEnum.EXT_PARAMS_UNIQUE_CHECK_FAIL.getValue(), "外部参数唯一性， 校验失败");
                    }
                }
            }
        }
    }

    @Override
    public void wxCheck(Integer source, String open_id, String app_id, ProjectDto project) {
        JSONObject customAttr = JSONObject.parseObject(project.getCustomAttr());
        boolean wxOnce = customAttr.getBooleanValue("wxOnce");
        if (!wxOnce || StrUtil.isEmptyIfStr(open_id)) {
            return;
        }
        boolean is_test_mode = source.equals(RESPONDENT_SOURCE_PREVIEW_VALUE);
        if (!is_test_mode && checkWxExisted(open_id, app_id, project)) {
            throw new BusinessException(SurveyErrorCodeEnum.WX_DUPLICATE_ANS.getValue(), "您使用的微信已经参与过该调查");
        }
    }

    //检查微信答题控制
    private boolean checkWxExisted(String open_id, String app_id, ProjectDto project) {
        if (ObjectUtils.isEmpty(project)) {
            return false;
        }
        JSONObject customAttr = JSONObject.parseObject(project.getCustomAttr());
        Boolean wxOnce = customAttr.getBoolean("wxOnce");
        if (wxOnce == null || !wxOnce) {
            return false;
        }
        String wxOnce_time = customAttr.getString("wxOnce_time");
        int wxOnce_num = customAttr.getIntValue("wxOnce_num");
        int current_ip_count;
        if (StrUtil.isBlank(wxOnce_time) && wxOnce_num < 1) {
            return false;
        }

        QueryWrapper<Integer> queryWrapper = new QueryWrapper<>();
        //SELECT count(1)  from
        //ds_survey_respondent a,
        //ds_survey_respondentweixin w
        //where a.id=w.rid
        //and w.project_id='607599f7aace70000c2b6003';
        //and w.open_id= 'oLKpXwKYP8mHMHRYH_M5IjrWAd8I'
        //and w.app_id='wx9cfe7c96286e2b54'
        //and a.status in (1,2)
        //and a.data_status=1;
        if (StrUtil.equals("only", wxOnce_time)) {
            queryWrapper.eq(StringUtils.hasText(project.getId()), "w.project_id", project.getId());
            queryWrapper.eq(StringUtils.hasText(open_id), "w.open_id", open_id);
            queryWrapper.eq(StringUtils.hasText(app_id), "w.app_id", app_id);
            queryWrapper.in("a.status", RespondentStatusEnum.getRespondentStatus4Check());
            queryWrapper.eq("a.data_status", RESPONDENT_DATA_STATUS_FORMAL.getValue());
            queryWrapper.apply("a.id=w.rid");
        } else {
            Map<String, LocalDateTime> res = calcTimeRange4CheckCunt(wxOnce_time);
            if (res.isEmpty()) {
                return false;
            }

            queryWrapper.eq(StringUtils.hasText(project.getId()), "w.project_id", project.getId());
            queryWrapper.eq(StringUtils.hasText(open_id), "w.open_id", open_id);
            queryWrapper.eq(StringUtils.hasText(app_id), "w.app_id", app_id);
            queryWrapper.in("a.status", RespondentStatusEnum.getRespondentStatus4Check());
            queryWrapper.eq("a.data_status", RESPONDENT_DATA_STATUS_FORMAL.getValue());
            queryWrapper.gt("finish_time", res.get("gt_time"));
            queryWrapper.lt("finish_time", res.get("lt_time"));
            queryWrapper.apply("a.id=w.rid");
        }

        current_ip_count = surveyRespondentWeixinMapper.joinCountBY(queryWrapper);
        return current_ip_count >= wxOnce_num;
    }

    @Override
    public List<Integer> qdesGetQlistWithPid(String projectId, boolean isPreview) {
        return qdesGrpcClient.getQlistWithProjectId(projectId, isPreview);
    }

    @Override
    public void setRspdWxInfo(Integer seq, JSONObject wx_user_info) {
        if (!ObjectUtils.isEmpty(wx_user_info)) {
            String key = String.format(RSPD_FORMAT, seq);
            redisService.hashSet(key, "wx_user_info", wx_user_info);
        }
    }

    @Override
    public JSONObject respondentCountAutoInc(String project_id, Map<String, Long> kwargs) {
        JSONObject ret = new JSONObject();
        Long finish_count = kwargs.get("count_finish");
        String key = String.format(RESP_FINISH_COUNT_FORMAT_KEY, project_id);
        Long count_finish;
        if (finish_count == 1) {
            count_finish = redisService.incr(key);
        } else {
            count_finish = redisService.decr(key);
        }

        SurveyRespondentCount resp_count = surveyRespondentCountMapper.selectOne(Wrappers.<SurveyRespondentCount>lambdaQuery()
                .eq(SurveyRespondentCount::getProjectId, project_id)
                .last("limit 1")
        );
        if (Objects.nonNull(resp_count)) {
            count_finish += resp_count.getCountFinish();
        }
        ret.put("project_id", project_id);
        ret.put("count_finish", count_finish);
        return ret;
    }

    @Override
    public <T> Pair<String, File> exportWithDynamicData(String fileName, List<List<List<String>>> head, List<List<List<T>>> exportDataList, List<String> sheetNameList, HttpServletResponse response, String ossPath) {
        File exportFile = ExcelExportUtil.exportWithDynamicData2LocalDir(fileName, head, exportDataList, sheetNameList);
        //上传到oss
        String url = storageService.upload(exportFile, Objects.requireNonNull(exportFile).getName(), ossPath, new Dict()).getUrl();
        if (Objects.nonNull(response)) {
            ExcelExportUtil.downloadTemplate(exportFile.getPath(), fileName, ExcelExportUtil.XLSX_SUFFIX, response);
        }
        return Pair.with(url, exportFile);
    }

    @Override
    public JSONObject getRspdWxInfo(Integer seq) {
        //获取微信用户信息
        String key = String.format(RSPD_FORMAT, seq);
        Object ret = redisService.hashGet(key, "wx_user_info");
        if (ObjectUtils.isEmpty(ret)) {
            return new JSONObject();
        }
        return (JSONObject) ret;
    }

    @Override
    public Integer getRspdStatus(Integer seq) {
        String key = String.format(RSPD_FORMAT, seq);
        Object o = redisService.hashGet(key, "status");
        if (ObjectUtils.isEmpty(o)) {
            return null;
        } else {
            return Integer.parseInt(o.toString());
        }
    }

    @Override
    public JSONObject getAnsweredAnswers(String pid, Integer seq) {
        JSONObject data = new JSONObject();
        JSONObject orig_answers = getAnsweredData(seq);
        if (!StrUtil.equals(orig_answers.getString("pid"), pid)) {
            throw new BusinessException(String.format("Invalid Seq[pid={%s}, seq={%s}]", pid, seq));
        }
        JSONObject answers = new JSONObject();
        JSONObject custom_answers = new JSONObject();
        String prefix = "custom_data__";
        int prelen = prefix.length();
        orig_answers.forEach((k, v) -> {
            if (NumberUtil.isNumber(k)) {
                answers.put(k, v);
            }
            if (StrUtil.startWith(k, prefix)) {
                custom_answers.put(k.substring(prelen), v);
            }
        });

        JSONArray question_list = null;
        Object temp = orig_answers.get("question_list");
        if (ObjectUtils.isEmpty(temp)) {
            question_list = new JSONArray();
        } else if (temp instanceof String) {
            question_list = JSONArray.parseArray(temp.toString());
        } else if (temp instanceof List) {
            question_list = (JSONArray) temp;
        }
        data.put("answered_answers", answers);
        data.put("custom_answers", custom_answers);
        data.put("question_list", question_list);
        return data;
    }

    @Override
    public JSONObject getAnsweredData(Integer seq) {
        //python 放入进去是字符串，在save_page_answers可以找到
        //tmp_answer_str = json.dumps(answer)
        //await rc.hset(table, qid, tmp_answer_str)
        //String key = String.format(RSPD_FORMAT, seq);
        //Map<Object, Object> ret = redisService.hashGetAll(key);
        //JSONObject orig_answers = new JSONObject();
        //ret.forEach((k, v) -> {
        //    if (NumberUtil.isNumber(k.toString())) {
        //        orig_answers.put(k.toString(), JSON.parse(v.toString()));
        //    } else {
        //        orig_answers.put(k.toString(), v);
        //    }
        //});
        //return orig_answers;

        String key = String.format(RSPD_FORMAT, seq);
        Map<Object, Object> ret = redisService.hashGetAll(key);
        if (ObjectUtils.isEmpty(ret)) {
            return null;
        }
        JSONObject orig_answers = new JSONObject();
        //java直接放JSON（JSONArray或者JSONObject都可能）
        //这里做了兼容，兼容python的写法
        ret.forEach((k, v) -> {
            if (NumberUtil.isNumber(k.toString())) {
                if (v instanceof String) {
                    orig_answers.put(k.toString(), JSON.parse(v.toString()));
                } else {
                    orig_answers.put(k.toString(), v);
                }
            } else {
                orig_answers.put(k.toString(), v);
            }
        });
        return orig_answers;
    }

    @Override
    public JSONArray getLastAnsweredPageQidlist(Integer seq) {
        String key = String.format(Q_PATH_FORMAT, seq);
        List<Object> result = redisService.listRange(key, -1, -1);
        if (!ObjectUtils.isEmpty(result)) {
            String[] parts = result.get(0).toString().split(":");
            if (!ObjectUtils.isEmpty(parts)) {
                String[] temp;
                if (parts.length > 1) {
                    temp = parts[1].split("\\|");
                } else {
                    temp = parts[0].split("\\|");
                }
                List<String> list = Arrays.asList(temp);
                return JSONArray.parseArray(JSON.toJSONString(list));
            }
        }
        return new JSONArray();
    }

    @Override
    public JSONArray getSortsBySeq(Integer seq) {
        String key = String.format(SORT_FORMAT, seq);
        Map<Object, Object> raw_sorts = redisService.hashGetAll(key);
        JSONArray sorts = new JSONArray();
        raw_sorts.forEach((k, v) -> {
            try {
                String[] arrys = k.toString().split("_", 2);
                JSONObject item = new JSONObject();
                item.put("type", arrys[0]);
                item.put("gid", arrys[1]);
                item.put("value", Integer.parseInt(v.toString()));
                sorts.add(item);
            } catch (Exception e) {
                throw new BusinessException(String.format("无效的__SORT_%s:%s:%s", seq, k, v));
            }
        });
        return sorts;
    }

    @Override
    public Object getNewQuestionOrder(String pid, Integer seq) {
        String key = String.format(QORDER_FORMAT, pid, seq);
        return redisService.get(key);
    }

    @Override
    public JSONArray getRspdGntList(Integer seq) {
        JSONArray ret = new JSONArray();
        String key = String.format(RSPD_FORMAT, seq);
        Object o = redisService.hashGet(key, "gnt_list");
        if (ObjectUtils.isEmpty(o)) {
            log.info("getRspdGntList is empty: seq={},result={}", seq, o);
        } else if (o instanceof String) {
            log.error("getRspdGntList type={}", "String");
            ret = JSONArray.parseArray(o.toString());
        } else if (o instanceof List) {
            log.error("getRspdGntList type={}", "List");
            ret = JSONArray.parseArray(JSON.toJSONString(o));
        } else {
            log.error("getRspdGntList seq={},result={}", seq, o);
        }
        log.error("getRspdGntList result={}", ret.toJSONString());
        return ret;
    }

    @Override
    public void checkQuestionNameList(JSONObject params) {
        String project_id = params.getString("surveyId");
        String version = params.getString("version");
        JSONObject answer = (params.get("answer") instanceof String) ? JSONObject.parseObject(params.getString("answer")) : params.getJSONObject("answer");
        //名单类型 缓存数据（0：白，1：黑)
        String _key_type = String.format(NAME_LIST_TYPE_FORMAT_KEY, project_id, version);
        Map<Object, Object> cache_gids = redisService.hashGetAll(_key_type);
        if (!ObjectUtils.isEmpty(cache_gids)) {
            cache_gids.forEach((gid, data) -> {
                String _gid = gid.toString();
                JSONObject _data;
                if (data instanceof String) {
                    _data = JSONObject.parseObject(data.toString());
                } else {
                    _data = (JSONObject) data;
                }
                boolean _gid_enable = _data.getBooleanValue("enable");
                if (!answer.containsKey(_gid)) {
                    return;
                }
                if (!_gid_enable) {
                    return;
                }
                //如果 非必填，提交的答案内容为空，则直接跳过
                if (!(answer.get(_gid) instanceof List)) {
                    return;
                }
                String _answer_content = answer.getJSONArray(_gid).getString(0);
                String _key = String.format(NAME_LIST_DATA_FORMAT_KEY, project_id, version, _gid);
                log.info("check_survey_name_list_key: " + _key);

                long exist = redisService.sGetSetSize(_key);
                if (exist > 0) {
                    boolean ret = checkNameListCache(_data.getIntValue("ntype"), _answer_content, _key);
                    if (!ret) {
                        throw new BusinessException(SurveyErrorCodeEnum.NAME_LIST_CHECK_FAIL.getValue(), "您不在答题范围，请重新填写或联系问卷管理员,gid=" + _gid);
                    }
                } else {
                    //黑白名单无缓存数据，走数据库校验 逻辑
                    List<QdesNameListDto> name_list_arry = qdesGrpcClient.listNameList(project_id, Integer.parseInt(version), Integer.parseInt(gid.toString()));
                    if (!ObjectUtils.isEmpty(name_list_arry) && name_list_arry.size() == 1) {
                        QdesNameListDto name_list = name_list_arry.get(0);
                        String name_data_str = name_list.getData();
                        if (StrUtil.isBlank(name_data_str)) {
                            return;
                        }
                        if (!JSONUtil.isTypeJSONArray(name_data_str)) {
                            return;
                        }
                        JSONArray name_data = JSONArray.parseArray(name_data_str);
                        int name_type = name_list.getNtype();
                        if (ObjectUtils.isEmpty(name_data)) {
                            return;
                        }

                        //把从数据库 查找到的数据，放入到 redis 中
                        _key = String.format(NAME_LIST_DATA_FORMAT_KEY, name_list.getProjectId(), name_list.getVersion(), name_list.getGid());
                        List<String> datalist = name_data.toJavaList(String.class);
                        redisService.setAddExpire(_key, 14, TimeUnit.DAYS, datalist.toArray());

                        boolean ret = checkNameListContent(_answer_content, name_type, name_data);
                        if (!ret) {
                            throw new BusinessException(SurveyErrorCodeEnum.NAME_LIST_CHECK_FAIL.getValue(), "您不在答题范围，请重新填写或联系问卷管理员,gid=" + _gid);
                        }
                    }
                }
            });
        }
    }

    private boolean checkNameListContent(String answer_content, int name_type, JSONArray name_data) {
        if (name_type == NAMELIST_NTYPE_WHITE_VALUE) {
            return name_data.contains(answer_content);
        } else if (name_type == NAMELIST_NTYPE_BLACK_VALUE) {
            return !name_data.contains(answer_content);
        }
        return false;
    }

    private boolean checkNameListCache(int name_type, String name_data, String _key) {
        if (name_type == NAMELIST_NTYPE_WHITE_VALUE) {
            return redisService.isMember(_key, name_data);
        } else if (name_type == NAMELIST_NTYPE_BLACK_VALUE) {
            return !redisService.isMember(_key, name_data);
        }
        return false;
    }

    /**
     * 对答案进行多个唯一性检查并保存,需要保证原子性
     * history: 历史数据统计进入UniqueAnswer
     */
    @Override
    public List<String> saveUniqAnswers(String project_id, Integer seq, JSONObject answers, JSONArray uniq_check_list, boolean history) {
        //懒得定义对象,用个四元元组定义
        Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> ua_dict = new HashMap<>();
        if (!ObjectUtils.isEmpty(uniq_check_list)) {
            uniq_check_list.forEach(it -> {
                JSONObject uniq_q = (JSONObject) it;
                Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> r_dict = uniqParseQuestion(uniq_q, answers);
                ua_dict.putAll(r_dict);
            });
        }
        if (!ObjectUtils.isEmpty(ua_dict)) {
            List<String> ua_id_list = new ArrayList<>();
            ua_dict.forEach((ua_cons_data, ua_data) -> {
                int qid = ua_cons_data.getValue0();
                int row_id = ua_cons_data.getValue1();
                int option_id = ua_cons_data.getValue2();
                for (int index = 0; index < ua_data.size(); index++) {
                    String data = ua_data.getString(index);
                    String _data = data.length() > 32 ? DigestUtil.md5Hex(data) : data;

                    SurveyUniqueAnswer query = new SurveyUniqueAnswer();
                    query.setProjectId(project_id);
                    query.setSeq(Long.valueOf(seq));
                    query.setQid(qid);
                    query.setRowId(row_id);
                    query.setOpId(option_id);
                    LambdaQueryWrapper<SurveyUniqueAnswer> queryWrap = new LambdaQueryWrapper<>(query);

                    SurveyUniqueAnswer update = new SurveyUniqueAnswer();
                    update.setData(_data);
                    try {
                        SurveyUniqueAnswer uniq_ans = surveyUniqueAnswerService.getOneAndUpdate(queryWrap, update, Boolean.TRUE);
                        if (!ObjectUtils.isEmpty(uniq_ans)) {
                            ua_id_list.add(uniq_ans.getId());
                        }
                    } catch (Exception e) {
                        //history=true没找到调用方
                        //if (history) {
                        //
                        //}
                        if (!ObjectUtils.isEmpty(ua_id_list)) {
                            surveyDeliverUniqueAnswerMapper.deleteBatchIds(ua_id_list);
                        }
                        throw new BusinessException(SurveyErrorCodeEnum.DUPLICATE_ANSWER.getValue(), "saveUniqAnswers 异常");
                    }
                }
            });
            return ua_id_list;
        }
        return null;
    }

    private Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> uniqParseQuestion(JSONObject uniq_q, JSONObject answers) {
        log.info("[uniqParseQuestion] enter uniq_q={}，answers={}", uniq_q.toJSONString(), answers.toJSONString());
        Map<Quartet<Integer, Integer, Integer, Boolean>, JSONArray> ua_dict = new HashMap<>();
        String qtype = uniq_q.getString("qtype");
        String qid = uniq_q.getString("qid");
        if (!UNIQ_QUESTION_PARSERS.containsKey(qtype)) {
            log.error("[uniqParseQuestion]");
            throw new BusinessException(SurveyErrorCodeEnum.PARSER_NOT_FOUND.getValue(), "cannot find parser for qtype=" + qtype);
        }

        if (StrUtil.isNotBlank(qid) && !ObjectUtils.isEmpty(answers) && answers.containsKey(qid)) {
            ua_dict = UNIQ_QUESTION_PARSERS.get(qtype).apply(uniq_q, answers.get(qid));
        }
        return ua_dict;
    }

    /**
     * custom_data_utils
     */
    @Override
    public void getCustomData(String pid, JSONArray custom_data_actions) {
        if (CollectionUtils.isEmpty(custom_data_actions)) {
            return;
        }
        custom_data_actions.forEach((tmp) -> {
            JSONObject custom_data_action = (JSONObject) tmp;
            Pair<LambdaQueryWrapper<SurveyCustomData>, Integer> ret = checkCustomDataAction(pid, custom_data_action);
            SurveyCustomData update = new SurveyCustomData();
            update.setStatus(ret.getValue1());
            try {
                SurveyCustomData data = surveyCustomDataService.getOneAndUpdate(ret.getValue0(), update);
                if (ObjectUtils.isEmpty(data)) {
                    log.error("[getCustomData] CustomData not found");
                    throw new BusinessException(String.valueOf(ReqErrorCodeEnum.NOT_FOUND.getCode()), "CustomData not found");
                }
            } catch (Exception e) {
                throw new BusinessException(e.getMessage());
            }
        });
    }

    private Pair<LambdaQueryWrapper<SurveyCustomData>, Integer> checkCustomDataAction(String pid, JSONObject custom_data_action) {
        String code = custom_data_action.getString("metaCode");
        if (ObjectUtils.isEmpty(code)) {
            log.error("metaCode is required");
            throw new BusinessException(String.valueOf(ReqErrorCodeEnum.PARAM_FORMAT.getCode()), "metaCode is required");
        }
        SurveyCustomMeta meta = surveyCustomMetaMapper.selectOne(Wrappers.<SurveyCustomMeta>lambdaQuery()
                .eq(SurveyCustomMeta::getProjectId, pid)
                .eq(SurveyCustomMeta::getCode, code)
                .last("limit 1"));
        if (ObjectUtils.isEmpty(meta)) {
            log.error(String.format("CustomMeta[project_id=%s,code=%s] not found", pid, code));
            throw new BusinessException(String.valueOf(ReqErrorCodeEnum.NOT_FOUND.getCode()), String.format("CustomMeta[project_id=%s,code=%s] not found", pid, code));
        }
        LambdaQueryWrapper<SurveyCustomData> query = new LambdaQueryWrapper<>();
        query.eq(SurveyCustomData::getMetaId, meta.getId());

        List<String> keys = new ArrayList<>();
        String indexes_str = meta.getIndexes();
        JSONArray indexes_arry = null;
        if (StrUtil.isNotBlank(indexes_str) && JSONUtil.isTypeJSONArray(indexes_str)) {
            indexes_arry = JSONArray.parseArray(indexes_str);
        }
        if (!ObjectUtils.isEmpty(indexes_arry)) {
            indexes_arry.forEach(it -> {
                Object val = custom_data_action.get(it.toString());
                if (!ObjectUtils.isEmpty(val)) {
                    keys.add(val.toString());
                }
            });
        }
        if (!ObjectUtils.isEmpty(indexes_arry) && ObjectUtils.isEmpty(keys)) {
            log.error("[checkCustomDataAction]" + indexes_arry.getString(0) + "is required");
            throw new BusinessException(String.valueOf(ReqErrorCodeEnum.PARAM_LOST.getCode()), indexes_arry.getString(0) + "is required");
        }

        int idx = 1;
        for (String key : keys) {
            if (idx == 1) {
                query.eq(SurveyCustomData::getKey1, key);
            }
            if (idx == 2) {
                query.eq(SurveyCustomData::getKey2, key);
            }
            if (idx > 2) {
                break;
            }
            idx++;
        }
        Pair<LambdaQueryWrapper<SurveyCustomData>, Integer> result;
        JSONObject fsm = JSONObject.parseObject(meta.getFsm());
        if (!ObjectUtils.isEmpty(fsm)) {
            Map<String, Object> data = custom_data_action.toJavaObject(new TypeReference<Map<String, Object>>() {
            });
            String action = custom_data_action.getOrDefault("action", "").toString();
            if (ObjectUtils.isEmpty(action)) {
                log.error("[checkCustomDataAction] action is required");
                throw new BusinessException(String.valueOf(ReqErrorCodeEnum.PARAM_LOST.getCode()), "action is required");
            }
            if (!fsm.containsKey(action)) {
                log.error("[checkCustomDataAction] action must be" + String.join(",", fsm.keySet()));
                throw new BusinessException(String.valueOf(ReqErrorCodeEnum.PARAM_RANGE.getCode()), "action must be" + String.join(",", fsm.keySet()));
            }
            JSONObject targetFsm = fsm.getJSONObject(action);
            Object dst_status = targetFsm.get("dst");
            if (ObjectUtils.isEmpty(dst_status)) {
                log.error("[checkCustomDataAction] without");
                throw new BusinessException(String.valueOf(ReqErrorCodeEnum.NOT_FOUND.getCode()), String.format("action[%s] without :dst", action));
            }
            JSONArray src_list = targetFsm.getJSONArray("src_list");
            if (!ObjectUtils.isEmpty(src_list)) {
                query.in(SurveyCustomData::getStatus, src_list);
            }
            if (targetFsm.getBooleanValue("check_sign")) {
                SignUtil.checkSign(data, meta.getSecretKey());
            }

            result = Pair.with(query, (Integer) dst_status);
        } else {
            result = Pair.with(query, custom_data_action.getInteger("status"));
        }
        return result;
    }

    @Override
    public void savePageAnswers(String pid, Integer seq, Object answer_str, String page_id, JSONArray page_qid_list, Integer version, boolean goback_flag, Object custom_answers, JSONArray question_list_input) {
        JSONObject rspd_answers = new JSONObject();
        String table = String.format(RSPD_FORMAT, seq);
        String time_table = String.format(RSPD_TIME_FORMAT, seq);

        Object save_pid = redisService.hashGet(table, "pid");
        Object old_question_list_obj = redisService.hashGet(table, "question_list");
        JSONArray old_question_list = new JSONArray();
        if (!ObjectUtils.isEmpty(old_question_list_obj)) {
            if ((old_question_list_obj instanceof String) && JSONUtil.isTypeJSONArray(old_question_list_obj.toString())) {
                old_question_list = JSONArray.parseArray(old_question_list_obj.toString());
            } else if (old_question_list_obj instanceof List) {
                old_question_list = (JSONArray) old_question_list_obj;
            }
        }

        JSONArray question_list;
        if (!ObjectUtils.isEmpty(question_list_input) && !ObjectUtils.isEmpty(old_question_list)) {
            JSONArray new_q_gid_list = new JSONArray();
            question_list_input.forEach(tmp -> {
                JSONObject q = JSON.parseObject(JSON.toJSONString(tmp));
                new_q_gid_list.add(q.getString("gid"));
            });
            Iterator<Object> iterator = old_question_list.iterator();
            while (iterator.hasNext()) {
                Object item = iterator.next();
                String t = ((JSONObject) item).getString("gid");
                if (new_q_gid_list.contains(t)) {
                    iterator.remove();
                }
            }
            old_question_list.addAll(question_list_input);
            question_list = new JSONArray(old_question_list);
        } else if (!ObjectUtils.isEmpty(question_list_input) || !ObjectUtils.isEmpty(old_question_list)) {
            question_list = ObjectUtils.isEmpty(question_list_input) ? old_question_list : question_list_input;
        } else {
            question_list = new JSONArray();
        }

        if (ObjectUtils.isEmpty(save_pid)) {
            SurveyRespondent rspd = surveyRespondentMapper.selectOne(new LambdaQueryWrapper<SurveyRespondent>()
                    .eq(SurveyRespondent::getProjectId, pid)
                    .in(SurveyRespondent::getStatus, ListUtil.of(RESPONDENT_STATUS_BEGIN_VALUE, RESPONDENT_STATUS_OPEN_VALUE))
                    .eq(SurveyRespondent::getSeq, seq)
                    .last("limit 1"));
            if (ObjectUtils.isEmpty(rspd)) {
                return;
            }
            if (!rspd.getProjectId().equals(pid)) {
                log.error("[savePageAnswers] invalid SEQ 1111");
                throw new BusinessException(SurveyErrorCodeEnum.INVALID_SEQ.getValue(), String.format("Invalid Seq[pid=%s, seq=%s]", pid, seq));
            }
            SurveyRespondentData rspd_data = surveyRespondentDataMapper.selectOne(new LambdaQueryWrapper<SurveyRespondentData>()
                    .eq(SurveyRespondentData::getProjectId, pid)
                    .eq(SurveyRespondentData::getSeq, seq)
                    .last("limit 1"));
            if (!ObjectUtils.isEmpty(rspd_data)) {
                rspd_answers.putAll(JSONObject.parseObject(rspd_data.getAnswers()));
            }
            rspd_answers.put("version", rspd.getVersion());
            rspd_answers.put("group_code", rspd.getGroupCode());
            rspd_answers.put("source", rspd.getSource());
            rspd_answers.put("pid", pid);
        } else if (!save_pid.toString().equals(pid)) {
            log.error("[savePageAnswers] invalid SEQ 222");
            throw new BusinessException(SurveyErrorCodeEnum.INVALID_SEQ.getValue(), String.format("Invalid Seq[pid=%s, seq=%s]", pid, seq));
        }

        JSONObject answer_dict = new JSONObject();
        if (!ObjectUtils.isEmpty(answer_str)) {
            if (answer_str instanceof String) {
                answer_dict = JSONObject.parseObject(answer_str.toString());
            } else {
                answer_dict = (JSONObject) answer_str;
            }
        }
        JSONObject custom_dict = new JSONObject();
        if (!ObjectUtils.isEmpty(custom_answers)) {
            if (custom_answers instanceof String) {
                custom_dict = JSONObject.parseObject(custom_answers.toString());
            } else {
                custom_dict = (JSONObject) custom_answers;
            }
        }

        Object status_obj = redisService.hashGet(table, "status");
        rspd_answers.forEach((key, value) -> {
            if (value != null) {
                redisService.hashSet(table, key, value);
            }
        });
        answer_dict.forEach((key, value) -> {
            if (value != null) {
                redisService.hashSet(time_table, key, DateUtil.currentSeconds());
                redisService.hashSet(table, key, value);
            }
        });
        custom_dict.forEach((key, value) -> {
            if (value != null) {
                redisService.hashSet(table, "custom_data__" + key, value);
            }
        });
        redisService.hashSet(table, "status", RESPONDENT_STATUS_BEGIN_VALUE);
        redisService.hashSet(table, "question_list", question_list);
        forward(seq, page_id, page_qid_list, goback_flag);
        redisService.setAdd("seq_" + pid, seq);
        redisService.setAdd("answered_pid", pid);
        int status;
        if (ObjectUtils.isEmpty(status_obj) || !NumberUtil.isNumber(status_obj.toString())) {
            status = RESPONDENT_STATUS_OPEN_VALUE;
        } else {
            status = Integer.parseInt(status_obj.toString());
        }
        if (status == RESPONDENT_STATUS_OPEN_VALUE) {
            //data = {
            //        'project_id': pid,
            //        'version': version,
            //        'seq': seq,
            //        'time': '',
            //        'prev': RESPONDENT_STATUS_OPEN,
            //        'current': RESPONDENT_STATUS_BEGIN,
            //}

            JSONObject mqData = new JSONObject();
            mqData.put("project_id", pid);
            mqData.put("version", version);
            mqData.put("seq", seq);
            mqData.put("time", "");
            mqData.put("prev", RESPONDENT_STATUS_OPEN_VALUE);
            mqData.put("current", RESPONDENT_STATUS_BEGIN_VALUE);

            surveyMessageSendService.surveyRespondentStatusChangedSend(mqData);
            //await redis_mq.publish(topics.survey_respondent_status_changed, data)
        }
    }

    private void forward(Integer seq, String page_id, JSONArray page_qid_list, boolean goback_flag) {
        if (ObjectUtils.isEmpty(seq) || ObjectUtils.isEmpty(page_id) || ObjectUtils.isEmpty(page_qid_list)) {
            return;
        }
        String key = String.format(Q_PATH_FORMAT, seq);
        if (goback_flag) {
            gobackTo(seq, page_id, page_qid_list);
        }

        String val = String.join("|", page_qid_list.toJavaList(String.class));
        redisService.lRightPush(key, page_id + ":" + val);
    }

    /**
     * python 代码写的有问题
     * page_qid_list["2|9"]
     */
    private void gobackTo(Integer seq, String page_id, JSONArray page_qid_list) {
        String table = String.format(RSPD_FORMAT, seq);
        String key = String.format(Q_PATH_FORMAT, seq);
        String page_prefix = page_id + ":";
        Set<String> need_clean_qid = new HashSet<>();
        while (true) {
            Object s = redisService.rPop(key);
            if (ObjectUtils.isEmpty(s)) {
                break;
            }
            String old_qid_list_str = s.toString();
            String[] st = old_qid_list_str.substring(page_prefix.length()).split("\\|");
            need_clean_qid.addAll(Arrays.asList(st));
            if (old_qid_list_str.startsWith(page_prefix)) {
                break;
            }
        }
        String[] st_old = String.join("|", page_qid_list.toJavaList(String.class)).split("\\|");
        Set<String> old = new HashSet<>(Arrays.asList(st_old));
        need_clean_qid.removeAll(old);
        if (!ObjectUtils.isEmpty(need_clean_qid)) {
            List<String> list = new ArrayList<>(need_clean_qid);
            redisService.hashDel(table, list);
        }
    }

    @Override
    public String getQuotaCheckpointsForPageAnswers(String project_id, JSONObject page_answers) {
        JSONObject checkpoints = surveyQuotaConditionService.getCheckpointsWithQuotas(project_id);
        if (ObjectUtils.isEmpty(checkpoints)) {
            return null;
        }
        Set<String> quota_id_list = new HashSet<>();
        Set<String> qgids = page_answers.keySet();
        if (!ObjectUtils.isEmpty(qgids)) {
            qgids.forEach(qgid -> {
                JSONArray quota_id_tmp = checkpoints.getJSONArray(qgid);
                if (!ObjectUtils.isEmpty(quota_id_tmp)) {
                    quota_id_list.addAll(quota_id_tmp.toJavaList(String.class));
                }
            });
        }
        return ObjectUtils.isEmpty(quota_id_list) ? null : String.join("|", quota_id_list);
    }

    @Override
    public int getClientType(String user_agent) {
        if (StrUtil.isBlank(user_agent)) {
            return RESPONDENT_CLIENT_TYPE_UNKNOWN_VALUE;
        }
        UserAgent userAgent = UserAgentUtil.parse(user_agent);
        Platform platform = userAgent.getPlatform();
        if (platform.isIPad()) {
            return RESPONDENT_CLIENT_TYPE_PAD_VALUE;
        }
        if (platform.isMobile() && !platform.isIPad()) {
            return RESPONDENT_CLIENT_TYPE_PHONE_VALUE;
        }
        return RESPONDENT_CLIENT_TYPE_PC_VALUE;
    }

    @Override
    public Pair<Boolean, List<SurveyQuotaCheckItemVo>> isQuotafull(String project_id, Integer seq, JSONObject answers_dict, String quota_id) {
        log.info("/s/ isQuotafull project_id={}，seq={}，answers_dict={}", project_id, seq, answers_dict.toJSONString());
        //答题过程中配额校验，如果没有配额检查点无需执行配验配额动作
        Pair<Boolean, List<SurveyQuotaCheckItemVo>> data = null;
        if (ObjectUtils.isEmpty(answers_dict)) {
            return Pair.with(false, null);
        }
        SurveyQuotaCondition one = surveyQuotaConditionMapper.selectOne(Wrappers.<SurveyQuotaCondition>lambdaQuery()
                .eq(SurveyQuotaCondition::getProjectId, project_id)
                .last("limit 1"));
        if (ObjectUtils.isEmpty(one)) {
            return Pair.with(false, null);
        }
        JSONObject checkpoints = surveyQuotaConditionService.getCheckpointsWithQuotas(project_id);
        log.info("/s/ getCheckpointsWithQuotas result={}", checkpoints.toJSONString());
        if (ObjectUtils.isEmpty(checkpoints)) {
            log.info("/s/ isQuotafull checkpoints is empty");
            return Pair.with(false, null);
        }
        List<SurveyQuotaCheckItemVo> quota_checkitem_list = surveyQuotaConditionService.getAllMatchQuotaCheckitemV2(project_id, JSON.toJSONString(answers_dict), quota_id);
        log.info("/s/ getAllMatchQuotaCheckitemV2 result={}", checkpoints.toJSONString());

        if (ObjectUtils.isEmpty(quota_checkitem_list)) {
            log.info("/s/ isQuotafull quota_checkitem_list is empty");
            return Pair.with(false, null);
        }
        List<String> checkitem_id_list = quota_checkitem_list.stream().map(SurveyQuotaCheckItemVo::getId).collect(Collectors.toList());
        log.info("/s/ isQuotafull checkitem_id_list={}", JSON.toJSONString(checkitem_id_list));
        List<Map<String, Object>> rs = surveyQuotaCheckItemMapper.getCountByQuotaCheckitemId(checkitem_id_list);
        log.info("/s/ isQuotafull rs={}", JSON.toJSONString(rs));
        HashMap<String, Integer> quota_checkitem_num_dict = new HashMap<>();
        rs.forEach(group -> quota_checkitem_num_dict.put(group.get("quota_checkitem_id").toString(), Integer.valueOf(group.get("count").toString())));
        for (SurveyQuotaCheckItemVo quota_checkitem : quota_checkitem_list) {
            Integer count = quota_checkitem.getNum() == null ? Integer.valueOf(0) : quota_checkitem.getNum();
            Integer finished = quota_checkitem_num_dict.getOrDefault(quota_checkitem.getId(), 0);
            if ((count - finished) < 1) {
                data = Pair.with(true, null);
                break;
            }
        }
        if (data != null) {
            return data;
        }
        return Pair.with(false, quota_checkitem_list);
    }

    @Override
    public String getGroupIdOnSurvey(String group_code, ProjectDto project) {
        log.info("getGroupIdOnSurvey group_code={}，orgID={}", group_code, project.getCompanyId());
        //String group_id = "";
        //if (!ObjectUtils.isEmpty(group_code)) {
        //    JSONObject params = new JSONObject();
        //    params.put("orgID", project.getCompanyId());
        //    params.put("code", group_code);
        //    params.put("status", ACTIVATED.getNumber());
        //    List<GroupDto> groups = userGrpcClient.listGroup(params);
        //    if (!ObjectUtils.isEmpty(groups)) {
        //        if (groups.size() == 1) {
        //            group_id = groups.get(0).getId();
        //        }
        //        if (groups.size() > 1) {
        //            log.error(String.format("org_id=%s, group code=%s duplicated", project.getCompanyId(), group_code));
        //        }
        //    }
        //}

        String group_id = "";
        if (!ObjectUtils.isEmpty(group_code)) {
            JSONObject params = new JSONObject();
            params.put("orgID", project.getCompanyId());
            params.put("code", group_code);
            GroupDto group = userGrpcClient.getGroup(params);
            if (group != null) {
                group_id = group.getId();
            }
        }

        log.info("getGroupIdOnSurvey group_id={}", group_id);
        return group_id;
    }

    @Override
    public Pair<SurveyRespondent, Boolean> submitRespondent(JSONObject param) {
        log.info("submitRespondent param={}", param.toJSONString());
        String project_id = param.getString("project_id");
        //python 用来回滚的，java不需要
        List<String> ua_id_list = new ArrayList<>();
        SurveyRespondent respondent = null;
        try {
            ua_id_list = saveUniqAnswers(project_id, param.getInteger("seq"), param.getJSONObject("total_answer"), param.getJSONArray("uniq_check_list"), false);
            JSONArray custom_data_action = param.getJSONArray("custom_data_action");
            getCustomData(project_id, custom_data_action);
            Pair<SurveyRespondent, Boolean> ret = createRespondentV2(param);
            respondent = ret.getValue0();
            boolean is_new = ret.getValue1();
            if (!is_new) {
                return Pair.with(respondent, false);
            }
        } catch (Exception e) {
            log.error("submitRespondent Exception", e);
            if (!ObjectUtils.isEmpty(ua_id_list)) {
                surveyDeliverUniqueAnswerMapper.deleteBatchIds(ua_id_list);
            }
        }
        //最终答卷保存成功，删除缓存
        log.info("submitRespondent redisDataCleanBySeq");
        redisDataCleanBySeq(project_id, param.getInteger("seq"));
        return Pair.with(respondent, true);
    }

    private Pair<SurveyRespondent, Boolean> createRespondentV2(JSONObject param) {
        log.info("createRespondentV2 param={}", param.toJSONString());
        int client_type = getClientType(param.getString("user_agent"));
        Integer seq = param.getInteger("seq");
        LocalDateTime tmp_created = getRspdCreated(seq);
        LocalDateTime created = tmp_created != null ? tmp_created : LocalDateTimeUtil.of(param.getDate("begin_time"));

        LocalDateTime tmp_finish_time = LocalDateTimeUtil.of(param.getDate("finish_time"));
        LocalDateTime finish_time = tmp_finish_time != null ? tmp_finish_time : LocalDateTime.now();
        String ip_country, ip_province, ip_city;
        String ip = param.getString("ip");
        log.info("createRespondentV2 ip={}", ip);
        try {
            //中国|江苏省|苏州市|电信
            String cityInfo = Ip2RegionUtil.getCityInfo(ip);
            String[] addr = cityInfo.split("\\|");
            ip_country = addr[0];
            ip_province = addr[1];
            ip_city = addr[2];
        } catch (Exception e) {
            log.error("ip归属地查询错误");
            ip_country = "局域网";
            ip_province = "局域网";
            ip_city = "局域网";
        }

        //可以搭配CompletableFuture.allOf
        JSONObject wx_info = getRspdWxInfo(seq);
        String ded_code = getRspdDedCode(seq);
        String idy_sid = getRspdIdySid(seq);
        JSONArray survey_gids = getRspdGntList(seq);
        String dlvr_rid = getRspdDlvrRid(seq);

        String project_id = param.getString("project_id");
        String deliver_id = param.getString("deliver_id");
        log.info("createRespondentV2 deliver_id={}", deliver_id);
        String deliver_group_id = param.getString("deliver_group_id");
        String member_id = param.getString("member_id");
        Integer source = param.getInteger("source");

        if (!ObjectUtils.isEmpty(deliver_id)) {
            //为了支持rpc查询，投放的答卷相关字段不能设置成null
            if (ObjectUtils.isEmpty(deliver_group_id)) {
                deliver_group_id = "";
            }
            if (ObjectUtils.isEmpty(member_id)) {
                member_id = "";
            }
        } else {
            //为了利用稀疏索引，非投放的答卷字段要设置成null
            deliver_id = null;
            deliver_group_id = null;
            member_id = null;
        }

        if (created == null) {
            return Pair.with(null, false);
        }

        log.info("createRespondentV2 project_id={}", project_id);
        SurveyRespondent entity = new SurveyRespondent();
        entity.setProjectId(project_id);
        entity.setVersion(param.getInteger("version"));
        entity.setUserAgent(param.getString("user_agent"));
        entity.setIp(ip);
        entity.setSubmitSeq(getNextSubmitSeq(project_id, param.getInteger("version")));
        entity.setIpCountry(ip_country);
        entity.setIpProvince(ip_province);
        entity.setIpCity(ip_city);
        entity.setClientType(client_type);
        entity.setGroupCode(param.getString("group_code"));
        entity.setDedCode(ded_code);
        entity.setIdySid(idy_sid);
        entity.setGroupId(param.getString("group_id"));
        entity.setBeginTime(created);
        entity.setFinishTime(finish_time);
        entity.setStatus(param.getInteger("status"));
        entity.setDataStatus(param.getInteger("data_status"));
        entity.setSource(source);
        entity.setPersonId(param.getString("person_id"));
        entity.setExtSeq(param.getString("ext_seq"));
        entity.setMemberId(member_id);
        entity.setDeliverGroupId(deliver_group_id);
        entity.setDeliverId(deliver_id);
        entity.setAgainDeliverRecordId(param.getString("again_deliver_record_id"));
        entity.setSurveyGids(ObjectUtils.isEmpty(survey_gids) ? "[]" : survey_gids.toJSONString());
        entity.setDlvrRid(dlvr_rid);

        Pair<SurveyRespondent, Boolean> c_ret = createRspd(param, entity, wx_info);
        SurveyRespondent respondent = c_ret.getValue0();
        Boolean is_new = c_ret.getValue1();
        if (!is_new) {
            return Pair.with(respondent, false);
        }

        Integer status = param.getInteger("status");
        setRspdStatus(seq, status);

        //订制投放外部参数数据记录到 DeliverUniqueAnswer
        JSONObject custom_answers = param.getJSONObject("custom_answer");
        saveDeliverUniqueAnswer(deliver_id, seq, custom_answers);

        //saas用
        /*if (Objects.equals(source, RESPONDENT_SOURCE_SAMPLE_VALUE)) {
            String q = null;
            if (Objects.equals(status, RESPONDENT_STATUS_FINISH_VALUE) || Objects.equals(status, RESPONDENT_STATUS_BACKUP_VALUE)) {
                q = "c";
            } else if (Objects.equals(status, RESPONDENT_STATUS_SCREEN_OUT_VALUE) || Objects.equals(status, RESPONDENT_STATUS_DISCARD_VALUE)) {
                q = "s";
            } else if (Objects.equals(status, RESPONDENT_STATUS_QUOTA_FULL_VALUE)) {
                q = "q";
            }
            if (!ObjectUtils.isEmpty(q)) {
                surveyUnionCallback(idy_sid, seq, q);
            }
        }*/

        return Pair.with(respondent, true);
    }

    @Override
    public void surveyUnionCallback(String idy_sid, Integer rid, String q) {
        //这个地址没找到
        //saas才有
    }

    @Override
    public void saveDeliverUniqueAnswer(String deliver_id, Integer seq, JSONObject custom_answers) {
        log.error("saveDeliverUniqueAnswer deliver_id={},  seq={},  custom_answers={}", deliver_id, seq, custom_answers);
        if (ObjectUtil.hasEmpty(deliver_id, seq, custom_answers)) {
            return;
        }
        custom_answers.forEach((param_name, param_value) -> {
            SurveyDeliverUniqueAnswer update = new SurveyDeliverUniqueAnswer();
            update.setExtParamVal(param_value.toString());
            update.setCreatedDt(LocalDateTime.now());
            int rowcount = surveyDeliverUniqueAnswerMapper.update(update, Wrappers.<SurveyDeliverUniqueAnswer>lambdaQuery()
                    .eq(SurveyDeliverUniqueAnswer::getDeliverId, deliver_id)
                    .eq(SurveyDeliverUniqueAnswer::getSeq, seq)
                    .eq(SurveyDeliverUniqueAnswer::getExtParamName, param_name));
            if (rowcount == 0) {
                //如果没有则创建
                SurveyDeliverUniqueAnswer item = new SurveyDeliverUniqueAnswer();
                item.setDeliverId(deliver_id);
                item.setSeq(seq);
                item.setExtParamName(param_name);
                item.setExtParamVal(param_value.toString());
                item.setCreatedDt(LocalDateTime.now());
                surveyDeliverUniqueAnswerMapper.insert(item);
            }
        });
    }

    @Override
    public void setRspdStatus(Integer seq, Integer status) {
        if (ObjectUtil.hasEmpty(seq, status)) {
            return;
        }
        String key = String.format(RSPD_FORMAT, seq);
        redisService.hashSet(key, "status", status);
    }

    //@Retryable(value = Exception.class)
    private Pair<SurveyRespondent, Boolean> createRspd(JSONObject param, SurveyRespondent data, JSONObject wx_info) {
        //python加了重试3次
        //@Retryable基于AOP实现，所以不支持类里自调用方法
        String project_id = data.getProjectId();
        Integer seq = param.getInteger("seq");
        Integer version = data.getVersion();
        String brand_info = param.getString("brand_info");
        String model_info = param.getString("model_info");

        SurveyRespondent rspd = surveyRespondentMapper.selectOne(new LambdaQueryWrapper<SurveyRespondent>()
                .eq(SurveyRespondent::getProjectId, project_id)
                .in(SurveyRespondent::getStatus, ListUtil.of(RESPONDENT_STATUS_BEGIN_VALUE, RESPONDENT_STATUS_OPEN_VALUE))
                .eq(SurveyRespondent::getSeq, seq)
                .last("limit 1"));
        if (ObjectUtils.isEmpty(rspd)) {
            rspd = new SurveyRespondent();
            //说明是新的, 指定一个id
            String rspd_id = new ObjectId().toHexString();
            data.setId(rspd_id);
            if (!StrUtil.isEmpty(brand_info) && !StrUtil.isEmpty(model_info)) {
                String extra_id = new ObjectId().toHexString();
                data.setRspdExtraId(extra_id);
                //这尼玛一辈子都走不到
                SurveyRespondentExtra it = new SurveyRespondentExtra();
                it.setId(extra_id);
                it.setProjectId(project_id);
                it.setRid(rspd_id);
                it.setBrandInfo(brand_info);
                it.setModelInfo(model_info);
                surveyRespondentExtraMapper.insert(it);
            }
            String rspd_data_id = new ObjectId().toHexString();
            data.setRspdDataId(rspd_data_id);

            JSONObject paramobjk = new JSONObject();
            paramobjk.put("id", rspd_data_id);
            paramobjk.put("rid", rspd_id);
            paramobjk.put("seq", seq);
            paramobjk.put("answers", param.get("total_answer"));
            paramobjk.put("custom_answers", param.get("custom_answer"));
            paramobjk.put("question_list", param.get("question_list"));
            paramobjk.put("project_id", project_id);
            createRespondentdata(paramobjk);

            SurveyRespondentWeixin insertWeixin = null;
            if (!ObjectUtils.isEmpty(wx_info)) {
                wx_info.put("project_id", project_id);
                wx_info.put("rid", rspd_id);
                insertWeixin = wx_info.toJavaObject(SurveyRespondentWeixin.class);
                if (Objects.isNull(insertWeixin.getWxSex())) {
                    insertWeixin.setWxSex(RESPONDENTWEIXIN_WX_SEX_UNKNOWN_VALUE);
                }
                surveyRespondentWeixinMapper.insert(insertWeixin);
                data.setRspdWeixinId(insertWeixin.getId());
            }
            data.setProjectId(project_id);
            data.setSeq(Long.valueOf(seq));
            //默认值插入
            if (ObjectUtils.isEmpty(data.getSource())) {
                data.setSource(RESPONDENT_SOURCE_UNKNOWN_VALUE);
            }
            if (ObjectUtils.isEmpty(data.getIsCopyDemo())) {
                data.setIsCopyDemo(Boolean.FALSE);
            }
            if (ObjectUtils.isEmpty(data.getSurveyGids())) {
                data.setSurveyGids("[]");
            }
            surveyRespondentMapper.insert(data);

            SurveyRespondent respondent = new SurveyRespondent();
            BeanUtil.copyProperties(data, respondent);

            if (!ObjectUtils.isEmpty(wx_info)) {
                rspd.setWxInfos(Collections.singletonList(insertWeixin));
            } else {
                rspd.setWxInfos(new ArrayList<>());
            }

            return Pair.with(respondent, true);
        } else {
            try {
                //根据id更新
                data.setId(rspd.getId());
                surveyRespondentMapper.updateById(data);
                surveyRespondentService.copyProperties(data, rspd);

                if (!ObjectUtils.isEmpty(wx_info)) {
                    wx_info.put("project_id", project_id);
                    wx_info.put("rid", rspd.getId());

                    SurveyRespondentWeixin query = new SurveyRespondentWeixin();
                    query.setRid(rspd.getId());
                    LambdaQueryWrapper<SurveyRespondentWeixin> queryWrap = new LambdaQueryWrapper<>(query);

                    SurveyRespondentWeixin update = wx_info.toJavaObject(SurveyRespondentWeixin.class);

                    SurveyRespondentWeixin wx_info_data = surveyRespondentWeixinService.getOneAndUpdate(queryWrap, update, Boolean.TRUE);
                    rspd.setWxInfos(Collections.singletonList(wx_info_data));
                } else {
                    rspd.setWxInfos(new ArrayList<>());
                }
            } catch (Exception e) {
                log.error("createRspd error111");
                rspd = surveyRespondentMapper.selectOne(new LambdaQueryWrapper<SurveyRespondent>()
                        .eq(SurveyRespondent::getProjectId, project_id)
                        .eq(SurveyRespondent::getSeq, seq)
                        .last("limit 1"));
                if (!ObjectUtils.isEmpty(rspd)) {
                    return Pair.with(rspd, false);
                }

                log.error("createRspd error222");
                //submit_seq冲突
                Long cur_seq = getMaxtSubmitSeq(project_id, version);
                while (true) {
                    Long submit_seq = getNextSubmitSeq(project_id, version);
                    if (submit_seq >= cur_seq) {
                        break;
                    }
                }
                log.error("createRspd error333");
                throw new BusinessException("db data duplicate");
            }

            SurveyRespondent update_data = new SurveyRespondent();
            if (!StrUtil.isEmpty(brand_info) && !StrUtil.isEmpty(model_info)) {
                SurveyRespondentExtra query = new SurveyRespondentExtra();
                query.setRid(rspd.getId());
                LambdaQueryWrapper<SurveyRespondentExtra> queryWrap = new LambdaQueryWrapper<>(query);
                //需要更新的
                SurveyRespondentExtra update = new SurveyRespondentExtra();
                update.setProjectId(project_id);
                update.setRid(rspd.getId());
                update.setBrandInfo(brand_info);
                update.setModelInfo(model_info);

                SurveyRespondentExtra item;
                try {
                    item = surveyRespondentExtraService.getOneAndUpdate(queryWrap, update, Boolean.TRUE);
                    update_data.setRspdExtraId(item.getId());
                } catch (Exception e) {
                    log.error("createRspd surveyRespondentExtraService.getOneAndUpdate error");
                    throw new BusinessException(e.getMessage());
                }

            }

            SurveyRespondentData query = new SurveyRespondentData();
            query.setRid(rspd.getId());
            LambdaQueryWrapper<SurveyRespondentData> queryWrap = new LambdaQueryWrapper<>(query);
            //需要更新的
            JSONObject update_params = new JSONObject();
            update_params.put("project_id", project_id);
            update_params.put("seq", seq);
            update_params.put("answers", param.get("total_answer"));
            update_params.put("custom_answers", param.get("custom_answer"));
            update_params.put("question_list", param.get("question_list"));

            SurveyRespondentData rspd_data = getOneAndUpdate4RespondentData(queryWrap, update_params, Boolean.TRUE);
            update_data.setRspdDataId(rspd_data.getId());

            update_data.setId(rspd.getId());
            surveyRespondentMapper.updateById(update_data);
            return Pair.with(rspd, true);
        }
    }

    @Override
    public SurveyRespondentData getOneAndUpdate4RespondentData(LambdaQueryWrapper<SurveyRespondentData> queryWrapper, JSONObject update_params, Boolean is_upsert) {
        SurveyRespondentData rspd_data = surveyRespondentDataMapper.selectOne(queryWrapper.last("limit 1"));

        Object question_list = update_params.get("question_list");
        update_params.remove("question_list");
        String hashkey = ObjectUtils.isEmpty(question_list) ? DigestUtil.md5Hex("[]") : DigestUtil.md5Hex(JSON.toJSONString(question_list));
        update_params.put("hash_q_list_key", hashkey);
        log.info("getOneAndUpdate4RespondentData update_params={}", update_params.toJSONString());
        JSONObject hash_question_dict = new JSONObject();
        hash_question_dict.put("hashkey", hashkey);
        hash_question_dict.put("question_list", question_list);

        if (!ObjectUtils.isEmpty(rspd_data) && !ObjectUtils.isEmpty(update_params)) {
            log.info("getOneAndUpdate4RespondentData 111111111111");
            createRelRespondenthashquestionlist(hash_question_dict);
            SurveyRespondentData ret = update_params.toJavaObject(SurveyRespondentData.class);
            ret.setId(rspd_data.getId());
            surveyRespondentDataMapper.updateById(ret);
            return ret;
        } else if (is_upsert) {
            log.info("getOneAndUpdate4RespondentData 222222222222");
            return createRelRespondentdata(update_params, hash_question_dict);
        } else {
            return rspd_data;
        }
    }

    @Override
    public SurveyRespondentData createRespondentdata(JSONObject params) {
        Object question_list = params.get("question_list");
        params.remove("question_list");
        String hashkey = ObjectUtils.isEmpty(question_list) ? DigestUtil.md5Hex("[]") : DigestUtil.md5Hex(JSON.toJSONString(question_list));
        params.put("hash_q_list_key", hashkey);

        JSONObject hash_question_dict = new JSONObject();
        hash_question_dict.put("hashkey", hashkey);
        hash_question_dict.put("question_list", question_list);
        return createRelRespondentdata(params, hash_question_dict);
    }

    @Override
    public SurveyRespondentData createRelRespondentdata(JSONObject data, JSONObject sub_data) {
        createRelRespondenthashquestionlist(sub_data);
        SurveyRespondentData result = data.toJavaObject(SurveyRespondentData.class);
        result.setAnswers(ObjectUtils.isEmpty(data.get("answers")) ? "{}" : JSON.toJSONString(data.get("answers")));
        result.setCustomAnswers(ObjectUtils.isEmpty(data.get("custom_answers")) ? "{}" : JSON.toJSONString(data.get("custom_answers")));
        result.setQuestionList(ObjectUtils.isEmpty(data.get("question_list")) ? "[]" : JSON.toJSONString(data.get("question_list")));
        result.setReportUrl(ObjectUtils.isEmpty(data.get("report_url")) ? "{}" : JSON.toJSONString(data.get("report_url")));
        surveyRespondentDataMapper.insert(result);
        return result;
    }

    @Override
    public void createRelRespondenthashquestionlist(JSONObject data) {
        log.error("createRelRespondenthashquestionlist data:{}", data.toJSONString());
        //校验并创建hashkey子表数据， 如果有不需创建及更新，没有插入
        SurveyRespondentHashQuestionList ret = surveyRespondentHashQuestionListMapper.selectOne(new LambdaQueryWrapper<SurveyRespondentHashQuestionList>()
                .eq(SurveyRespondentHashQuestionList::getHashkey, data.getString("hashkey"))
                .last("limit 1"));
        if (ObjectUtils.isEmpty(ret)) {
            SurveyRespondentHashQuestionList it = data.toJavaObject(SurveyRespondentHashQuestionList.class);
            if (ObjectUtils.isEmpty(it.getQuestionList())) {
                it.setQuestionList("[]");
                it.setHashkey(DigestUtil.md5Hex("[]"));
            }
            surveyRespondentHashQuestionListMapper.insert(it);
        }
    }

    @Override
    public Long getNextSubmitSeq(String project_id, Integer version) {
        Integer seq = serviceSequenceService.getSequence(String.format(SUBMIT_SEQ_FORMAT_KEY, project_id, version), 1);
        return Convert.toLong(seq);
    }

    @Override
    public Long getMaxtSubmitSeq(String project_id, Integer version) {
        SurveyRespondent cursor = surveyRespondentMapper.selectOne(new LambdaQueryWrapper<SurveyRespondent>()
                .eq(SurveyRespondent::getProjectId, project_id)
                .eq(SurveyRespondent::getVersion, version)
                .orderByDesc(SurveyRespondent::getSubmitSeq)
                .last("limit 1"));
        if (ObjectUtils.isEmpty(cursor)) {
            return 0L;
        } else {
            return cursor.getSubmitSeq();
        }
    }

    @Override
    public String getRspdDlvrRid(Integer seq) {
        //获取投放记录id
        String key = String.format(RSPD_FORMAT, seq);
        Object ret = redisService.hashGet(key, "dlvr_rid");
        if (!ObjectUtils.isEmpty(ret)) {
            return ret.toString();
        }
        return null;
    }

    @Override
    public String getRspdIdySid(Integer seq) {
        String key = String.format(RSPD_FORMAT, seq);
        Object ret = redisService.hashGet(key, "idy_sid");
        if (!ObjectUtils.isEmpty(ret)) {
            return ret.toString();
        }
        return null;
    }

    @Override
    public String getRspdDedCode(Integer seq) {
        //获取专用链接投放的ded_code
        String key = String.format(RSPD_FORMAT, seq);
        Object ret = redisService.hashGet(key, "ded_code");
        if (!ObjectUtils.isEmpty(ret)) {
            return ret.toString();
        }
        return null;
    }

    @Override
    public Integer checkAutoFilter(String project_id, JSONObject answers, Integer source, String group_id, JSONObject custom_answers) {
        //检查自动删除器
        SurveyFilter auto_filter = surveyFilterMapper.selectOne(Wrappers.<SurveyFilter>lambdaQuery()
                .eq(SurveyFilter::getProjectId, project_id)
                .last("limit 1"));
        if (!ObjectUtils.isEmpty(auto_filter) && auto_filter.getStatus() == FILTER_STATUS_OPEN_VALUE) {
            return filterRespondent(auto_filter, answers, source, group_id, custom_answers);
        }
        return null;
    }

    private int filterRespondent(SurveyFilter filt, JSONObject answer, Integer source, String group_id, JSONObject custom_answer) {
        JSONObject filtdic = JSONObject.parseObject(JSON.toJSONString(filt));
        Boolean ret = null;
        if (!ObjectUtils.isEmpty(filtdic.get("ext"))) {
            JSONObject ext_dic = filtdic.getJSONObject("ext");
            String ctype = ext_dic.getString("ctype");
            JSONArray ext_list = ext_dic.getJSONArray("v");
            List<Boolean> check_list = new ArrayList<>();
            AtomicReference<Boolean> sub = new AtomicReference<>(Boolean.FALSE);
            ext_list.forEach((ext_tmp) -> {
                JSONObject ext = (JSONObject) ext_tmp;
                String name = ext.getString("name");
                String ptype = ext.getString("ptype");
                if (custom_answer.containsKey(name)) {
                    if (StrUtil.equals(ptype, "$in")) {
                        sub.set(ext.getJSONArray("v").contains(custom_answer.getString(name)));
                    } else if (StrUtil.equals(ptype, "$nin")) {
                        sub.set(!ext.getJSONArray("v").contains(custom_answer.getString(name)));
                    }
                }
                check_list.add(sub.get());
            });

            if (!ObjectUtils.isEmpty(check_list)) {
                int size = check_list.size();
                Boolean[] all = check_list.toArray(new Boolean[size]);

                if (StrUtil.equals(ctype, "$and")) {
                    ret = BooleanUtil.andOfWrap(all);
                } else {
                    ret = BooleanUtil.orOfWrap(all);
                }
            }
        }

        if ((ret == null || Boolean.TRUE.equals(ret)) && !ObjectUtils.isEmpty(filtdic.get("sourceList"))) {
            ret = filtdic.getJSONArray("sourceList").contains(source);
        }

        if ((ret == null || Boolean.TRUE.equals(ret)) && !ObjectUtils.isEmpty(filtdic.get("groupList"))) {
            ret = filtdic.getJSONArray("groupList").contains(group_id);
        }


        if ((ret == null || Boolean.TRUE.equals(ret)) && !ObjectUtils.isEmpty(filtdic.get("qlist"))) {
            List<Boolean> q_ret = new ArrayList<>();
            JSONArray qlist_arry = filtdic.getJSONArray("qlist");
            AtomicReference<Boolean> sub = new AtomicReference<>(Boolean.FALSE);
            qlist_arry.forEach(dataobj -> {
                JSONObject data = (JSONObject) dataobj;
                String qid = data.getString("id");
                if (StrUtil.equals(data.getString("op"), "select") && StrUtil.equals(data.getString("qtype"), "score")) {
                    qid = StrUtil.removeSuffix(qid, "_tag");
                }
                Object qans = getAnswerById(answer, qid);
                if (ObjectUtils.isEmpty(qans)) {
                    return;
                }

                String op = data.getString("op");
                Object val = data.get("v");
                BiFunction<Object, Object, Boolean> filt_select = FILT_MATCHER.get("select");
                if (StrUtil.equals("range", op)) {
                    JSONArray val_arry = data.getJSONArray("v");
                    boolean left_open = data.getBooleanValue("leftOpen");
                    boolean right_open = data.getBooleanValue("rightOpen");
                    List<Integer> l;
                    if (!left_open && !right_open) {
                        l = Convert.toList(Integer.class, NumberUtil.range(val_arry.getInteger(0), val_arry.getInteger(1)));
                    } else if (!left_open) {
                        l = Convert.toList(Integer.class, NumberUtil.range(val_arry.getInteger(0), val_arry.getInteger(1) - 1));
                    } else if (!right_open) {
                        l = Convert.toList(Integer.class, NumberUtil.range(val_arry.getInteger(0) + 1, val_arry.getInteger(1)));
                    } else {
                        l = Convert.toList(Integer.class, NumberUtil.range(val_arry.getInteger(0) + 1, val_arry.getInteger(1) - 1));
                    }
                    val_arry.clear();
                    val_arry.addAll(l);
                    val = val_arry;
                }
                BiFunction<Object, Object, Boolean> function = FILT_MATCHER.getOrDefault(op, filt_select);
                if (function.apply(qans, val)) {
                    sub.set(Boolean.TRUE);
                } else {
                    sub.set(Boolean.FALSE);
                }
                q_ret.add(sub.get());
            });
            if (!ObjectUtils.isEmpty(q_ret)) {
                int size = q_ret.size();
                Boolean[] all = q_ret.toArray(new Boolean[size]);
                if (StrUtil.equals(filtdic.getString("qLogic"), "or")) {
                    ret = BooleanUtil.orOfWrap(all);
                } else {
                    ret = BooleanUtil.andOfWrap(all);
                }
            }
        }

        if ((ret == null || Boolean.TRUE.equals(ret)) && !ObjectUtils.isEmpty(filtdic.get("condList"))) {
            List<Boolean> c_ret = new ArrayList<>();
            JSONArray condList_arry = filtdic.getJSONArray("condList");
            List<String> cond_id_list = new ArrayList<>();
            condList_arry.forEach(it -> cond_id_list.add(((JSONObject) it).getString("id")));
            List<SurveyCondition> lists = surveyConditionMapper.selectList(Wrappers.<SurveyCondition>lambdaQuery().in(SurveyCondition::getId, cond_id_list));
            Map<String, SurveyCondition> cond_dict = lists.stream().collect(Collectors.toMap(SurveyCondition::getId, Function.identity()));
            AtomicReference<Boolean> sub = new AtomicReference<>(Boolean.FALSE);
            condList_arry.forEach(it -> {
                JSONObject val = (JSONObject) it;
                String cond_id = val.getString("id");
                val.remove("id");
                if (ObjectUtils.isEmpty(val)) {
                    return;
                }
                SurveyCondition cond = cond_dict.get(cond_id);
                if (ObjectUtils.isEmpty(cond)) {
                    return;
                }
                Object cond_val = calCondValByAnswer(answer, cond);
                BiFunction<Object, Object, Boolean> matcher = COND_MATCHER.get(val.getString("op"));
                if (matcher != null) {
                    if (matcher.apply(cond_val, val.get("v"))) {
                        sub.set(Boolean.TRUE);
                    } else {
                        sub.set(Boolean.FALSE);
                    }
                    c_ret.add(sub.get());
                }
            });
            if (!ObjectUtils.isEmpty(c_ret)) {
                int size = c_ret.size();
                Boolean[] all = c_ret.toArray(new Boolean[size]);
                if (StrUtil.equals(filtdic.getString("condLogic"), "or")) {
                    ret = BooleanUtil.orOfWrap(all);
                } else {
                    ret = BooleanUtil.andOfWrap(all);
                }
            }
        }

        if (filtdic.getInteger("action").equals(FILTER_ACTION_FITDISCARD_VALUE)) {
            return Boolean.TRUE.equals(ret) ? RESPONDENT_STATUS_DISCARD_VALUE : RESPONDENT_STATUS_FINISH_VALUE;
        } else if (filtdic.getInteger("action").equals(FILTER_ACTION_UNFITDISCARD_VALUE)) {
            return (ret == null || Boolean.TRUE.equals(ret)) ? RESPONDENT_STATUS_FINISH_VALUE : RESPONDENT_STATUS_DISCARD_VALUE;
        } else {
            return RESPONDENT_STATUS_FINISH_VALUE;
        }
    }

    private Object calCondValByAnswer(JSONObject answer, SurveyCondition cond) {
        Integer rule = cond.getRule();
        //["2_9", "12_13"]
        JSONArray qlist = JSONArray.parseArray(cond.getQlist());
        List<Integer> vals = new ArrayList<>();
        qlist.forEach(qid -> {
            Object v = getAnswerById(answer, qid.toString());
            vals.add(convert2Number(v));
        });
        Object val = null;
        if (rule.equals(CONDITION_RULE_SUM_VALUE)) {
            val = vals.stream().mapToInt(t -> t).sum();
        } else if (rule.equals(CONDITION_RULE_AVERAGE_VALUE)) {
            val = vals.stream().collect(Collectors.averagingDouble(e -> e));
        } else if (rule.equals(CONDITION_RULE_WAVERAGE_VALUE)) {
            JSONArray weights = JSONArray.parseArray(cond.getWeights());
            int max = Math.min(weights.size(), vals.size());
            BigDecimal sum = new BigDecimal("0");
            for (int i = 0; i < max; i++) {
                BigDecimal it = NumberUtil.mul(vals.get(i).toString(), weights.getString(i));
                sum = NumberUtil.add(sum, it);
            }
            val = sum;
        }
        return val;
    }

    private Integer convert2Number(Object arg) {
        if (ObjectUtils.isEmpty(arg)) {
            return 0;
        }
        Object v = arg;
        if (arg instanceof List) {
            v = ((JSONArray) arg).get(0);
        }
        return Integer.parseInt(v.toString());
    }

    /**
     * 根据问题的组合id获取对应的答案
     *
     * @param answer 答题端提交的答案
     * @param qid    {问题gid}_{矩阵行gid}_{选项seq}
     * @return java.lang.Object
     * @author chenglong.yue <chenglong.yue@idiaoyan.com>
     * @date 2023/5/10 9:50
     */
    private Object getAnswerById(JSONObject answer, String qid) {
        log.error("[getAnswerById] answer={},qid={}", answer.toJSONString(), qid);
        //python 说取第一个就行
        //AtomicReference<Object> ret = new AtomicReference<>();
        //Arrays.stream(qid.split("_")).findFirst().ifPresent(key -> ret.set(answer.get(key)));
        //return ret.get();

        String[] ids = qid.split("_");
        Object ret = answer;
        for (String key : ids) {
            ret = answer.get(key);
            if (ret == null) {
                ret = answer;
                break;
            }
            if (ret instanceof Map) {
                answer = (JSONObject) ret;
            }
        }
        return ret;
    }

    @Override
    public SurveyRespondent discardAutoFilter(SurveyRespondent rspd) {
        try {
            if (ObjectUtils.isEmpty(rspd) || ObjectUtils.isEmpty(rspd.getId())) {
                return null;
            }
            Integer old_status = rspd.getStatus();
            //await trigger_rspd_status(rspd, 'discard', project_id=rspd.project_id)
            triggerRspdStatus(rspd, "discard", rspd.getProjectId());

            SurveyRespondent query = new SurveyRespondent();
            query.setId(rspd.getId());
            LambdaQueryWrapper<SurveyRespondent> queryWrap = new LambdaQueryWrapper<>(query);
            //需要更新的
            SurveyRespondent update = new SurveyRespondent();
            update.setPreDiscardStatus(old_status);

            return surveyRespondentService.getOneAndUpdate(queryWrap, update);
        } catch (Exception e) {
            throw new BusinessException(e.getMessage());
        }
    }

    @Override
    public void triggerRspdStatus(SurveyRespondent rspd, String action, String project_id) throws Exception {
        if (ObjectUtils.isEmpty(rspd) || ObjectUtils.isEmpty(rspd.getId()) || ObjectUtils.isEmpty(project_id)) {
            return;
        }

        Integer cur_status = event2StatusMap.get(action);
        //python状态机写法，具体实现在ondiscard
        //看了代码action全是discard
        Integer old_status = rspd.getStatus();

        SurveyRespondent query = new SurveyRespondent();
        query.setId(rspd.getId());
        query.setStatus(old_status);
        LambdaQueryWrapper<SurveyRespondent> queryWrap = new LambdaQueryWrapper<>(query);
        //需要更新的
        SurveyRespondent update = new SurveyRespondent();
        update.setStatus(cur_status);
        rspd.setStatus(cur_status);

        surveyRespondentService.getOneAndUpdate(queryWrap, update);

        mqSurveyRespondentStatusChanged(rspd, old_status);
    }

    @Override
    public void mqSurveyRespondentStatusChanged(SurveyRespondent rspd, Integer old_status) {
        JSONObject data = new JSONObject();
        data.put("project_id", rspd.getProjectId());
        data.put("version", rspd.getVersion());
        data.put("seq", rspd.getSeq());
        data.put("time", LocalDateTimeUtil.formatNormal(rspd.getCreatedDt()));
        data.put("prev", old_status);
        data.put("current", rspd.getStatus());

        surveyMessageSendService.surveyRespondentStatusChangedSend(data);
        //await redis_mq.publish(topics.survey_respondent_status_changed, data)

        SurveyRespondentData respondent_data = surveyRespondentDataMapper.selectOne(new LambdaQueryWrapper<SurveyRespondentData>()
                .eq(SurveyRespondentData::getProjectId, rspd.getProjectId())
                .eq(SurveyRespondentData::getSeq, rspd.getSeq())
                .last("limit 1"));
        if (Objects.nonNull(respondent_data)) {
            mqSurveyRespondentModify(rspd, JSONObject.parseObject(respondent_data.getAnswers()), JSONObject.parseObject(respondent_data.getCustomAnswers()));
        }
    }

    @Override
    public void saveRespondentpropDelay(SurveyRespondent respondent, String project_id, Integer seq, JSONObject total_answer, String deliver_id, String deliver_group_id, String member_id, JSONObject custom_answer) {
        JSONObject props = new JSONObject();
        props.put("version", respondent.getVersion());
        props.put("group_id", respondent.getGroupId());
        props.put("source", respondent.getSource());
        props.put("status", respondent.getStatus());
        if (StrUtil.isNotBlank(deliver_id)) {
            props.put("deliver_id", deliver_id);
        }
        if (StrUtil.isNotBlank(deliver_group_id)) {
            props.put("deliver_group_id", deliver_group_id);
        }
        if (StrUtil.isNotBlank(member_id)) {
            props.put("member_id", member_id);
        }
        JSONObject open_props = new JSONObject();
        if (!CollectionUtils.isEmpty(custom_answer)) {
            custom_answer.forEach((k, v) -> open_props.put("custom_data__" + k, v));
        }
        //无返回值异步任务
        CompletableFuture.runAsync(() -> saveRspdPropTaskWrapper(project_id, seq, null, total_answer, props, open_props, null, respondent.getBeginTime()), saveRspdPropTaskExecutor);
    }

    @Override
    public void mqSurveyRespondentSubmit(SurveyRespondent rspd, JSONObject total_answer, JSONObject custom_answer, Boolean is_copy) {
        if (is_copy == null) {
            is_copy = Boolean.FALSE;
        }
        if (ObjectUtils.isEmpty(rspd)) {
            return;
        }

        JSONObject mq_data = getRespondentMqData(rspd, total_answer, custom_answer, is_copy);

        //await redis_mq.publish(topics.survey_respondent_submit, mq_data)
        surveyMessageSendService.surveyRespondentSubmitSend(mq_data);

        memberFootPrintMq(rspd, total_answer, custom_answer);
    }

    @Override
    public void mqSurveyRespondentModify(SurveyRespondent rspd, JSONObject total_answer, JSONObject custom_answer) {

        if (ObjectUtils.isEmpty(rspd)) {
            return;
        }

        JSONObject mq_data = getRespondentMqData(rspd, total_answer, custom_answer, null);
        surveyMessageSendService.surveyRespondentModifySend(mq_data);
        //await redis_mq.publish(topics.survey_respondent_modify, mq_data)
    }

    @Override
    public void memberFootPrintMq(SurveyRespondent rspd, JSONObject total_answer, JSONObject custom_answer) {
        log.error("memberFootPrintMq rspd={},total_answer={}", JSON.toJSONString(rspd), total_answer.toJSONString());
        String member_id = rspd.getMemberId();
        if (!ObjectUtils.isEmpty(member_id)) {
            Project project = qdesGrpcClient.getProject(rspd.getProjectId());
            List<QstructsDto> qStructList = qdesGrpcClient.listQStruct(rspd.getProjectId(), project.getReleaseVer());
            if (ObjectUtil.isEmpty(qStructList) || qStructList.size() > 1) {
                return;
            }

            QstructsDto qstruct = qStructList.get(0);
            String qstruct_id = qstruct.getId();
            List<Question> questions = getScoreQuestions(qstruct_id);

            List<String> satisfaction_gids = new ArrayList<>();
            List<String> nps_gids = new ArrayList<>();
            Map<String, Question> gid_question_map = new HashMap<>();
            questions.forEach(question -> {
                JSONObject dataobj = JSONObject.parseObject(question.getData());
                String gid = dataobj.getString("gid");
                if (StrUtil.equals(dataobj.getString("custom_qtype"), "nps")) {
                    nps_gids.add(gid);
                } else {
                    satisfaction_gids.add(gid);
                }
                gid_question_map.put(gid, question);
            });

            //获取第一到 满意度和nps题的gid
            Pair<String, String> pair = getFirstSatisfActionNpsGid(qstruct, satisfaction_gids, nps_gids);
            String first_satisfaction_gid = pair.getValue0();
            String first_nps_gid = pair.getValue1();

            String question_title = null;
            String question_type = null;
            int total_score = 0;
            int score = 0;
            //有nps的题目
            if (!ObjectUtils.isEmpty(first_nps_gid)) {
                Question question = gid_question_map.get(first_nps_gid);
                JSONObject dataobj = JSONObject.parseObject(question.getData());
                question_title = dataobj.getString("title");
                question_type = "nps";
                //{"10":{"33":[7,"open_text","reason_text"]}}取出7即可
                if (!ObjectUtils.isEmpty(total_answer)) {
                    AtomicInteger score_tmp = new AtomicInteger();
                    Optional.ofNullable(total_answer.getJSONObject(first_nps_gid)).
                            flatMap(obj -> obj.values().stream().findFirst()).
                            ifPresent(it -> score_tmp.set(Integer.parseInt(((JSONArray) it).get(0).toString())));
                    score = score_tmp.get();
                }
                total_score = 10;
            }

            if (!ObjectUtils.isEmpty(first_satisfaction_gid) && ObjectUtils.isEmpty(first_nps_gid)) {
                Question question = gid_question_map.get(first_satisfaction_gid);
                JSONObject dataobj = JSONObject.parseObject(question.getData());
                String qtype = dataobj.getString("qtype");
                question_title = dataobj.getString("title");
                question_type = "satisfaction";
                if (StrUtil.equals(qtype, "evaluation")) {
                    if (!ObjectUtils.isEmpty(total_answer)) {
                        score = total_answer.getJSONObject(first_satisfaction_gid).getIntValue("score");
                    }
                    total_score = dataobj.getIntValue("custom_score");
                } else {
                    if (!ObjectUtils.isEmpty(total_answer)) {
                        AtomicInteger score_tmp = new AtomicInteger();
                        Optional.ofNullable(total_answer.getJSONObject(first_satisfaction_gid)).
                                flatMap(obj -> obj.values().stream().findFirst()).
                                ifPresent(it -> score_tmp.set(Integer.parseInt(((JSONArray) it).get(0).toString())));
                        score = score_tmp.get();
                    }
                    JSONArray arry = dataobj.getJSONArray("score_range");
                    AtomicInteger total_score_tmp = new AtomicInteger();
                    Optional.ofNullable(arry).flatMap(obj -> obj.stream().skip(arry.size() - 1).findFirst()).
                            ifPresent(it -> total_score_tmp.set(Integer.parseInt(it.toString())));
                    total_score = total_score_tmp.get();
                }
            }

            JSONObject content = new JSONObject();
            content.put("project_id", project.getId());
            content.put("title", project.getTitle());
            content.put("rspd_status", rspd.getStatus());

            if (!ObjectUtils.isEmpty(question_title)) {
                content.put("question_title", question_title);
                content.put("question_type", question_type);
                content.put("score", score);
                content.put("total_score", total_score);
            }

            JSONObject member_footprint_data = new JSONObject();
            member_footprint_data.put("org_id", project.getCompanyId());
            member_footprint_data.put("member_id", member_id);
            member_footprint_data.put("type", MEMBER_FOOTPRINT_TYPE_SURVEY.getValue());
            member_footprint_data.put("source_key", rspd.getSeq());
            member_footprint_data.put("content", content);
            member_footprint_data.put("footprint_time", LocalDateTimeUtil.formatNormal(rspd.getCreatedDt()));
            member_footprint_data.put("is_copy_demo", rspd.getIsCopyDemo());

            log.info("提交答卷：mq发布联系人足迹--{}", member_footprint_data);
            surveyMessageSendService.memberFootprintSaveSend(member_footprint_data);
            //await redis_mq.publish(member_footprint_save, member_footprint_data)
        }
    }

    @Override
    public Pair<String, String> getFirstSatisfActionNpsGid(QstructsDto qstruct, List<String> satisfaction_gids, List<String> nps_gids) {
        log.error("getFirstSatisfActionNpsGid 入参:qstruct.data={},satisfaction_gids={},nps_gids={}", qstruct.getData(), JSON.toJSONString(satisfaction_gids), JSON.toJSONString(nps_gids));
        //获取第一个满意度的题型gid,和nps题型gid
        String first_satisfaction_gid = null, first_nps_gid = null;
        JSONObject data = JSONObject.parseObject(qstruct.getData());
        JSONArray itemsList = data.getJSONArray("items");
        Objects.requireNonNull(itemsList);
        for (int i = 0; i < itemsList.size(); i++) {
            JSONObject item = itemsList.getJSONObject(i);
            if (StrUtil.equals(item.getString("type"), "G")) {
                JSONArray tmparry = item.getJSONArray("items");
                Objects.requireNonNull(tmparry);
                for (int j = 0; j < tmparry.size(); j++) {
                    JSONObject tmp = tmparry.getJSONObject(j);
                    if (ObjectUtils.isEmpty(first_satisfaction_gid) && satisfaction_gids.contains(tmp.getString("gid"))) {
                        first_satisfaction_gid = tmp.getString("gid");
                    }
                    if (ObjectUtils.isEmpty(first_nps_gid) && nps_gids.contains(tmp.getString("gid"))) {
                        first_nps_gid = tmp.getString("gid");
                    }
                }
            } else if (StrUtil.equals(item.getString("type"), "Q")) {
                if (ObjectUtils.isEmpty(first_satisfaction_gid) && satisfaction_gids.contains(item.getString("gid"))) {
                    first_satisfaction_gid = item.getString("gid");
                }
                if (ObjectUtils.isEmpty(first_nps_gid) && nps_gids.contains(item.getString("gid"))) {
                    first_nps_gid = item.getString("gid");
                }
            }
        }
        return Pair.with(first_satisfaction_gid, first_nps_gid);
    }

    @Override
    public List<Question> getScoreQuestions(String qstruct_id) {
        List<Question> questionList = qdesGrpcClient.getQuestionList(qstruct_id);
        return questionList.stream().filter(item -> {
            JSONObject dict = JSONObject.parseObject(item.getData());
            return !ObjectUtils.isEmpty(dict) && Lists.newArrayList("star_score", "evaluation", "cursor_score", "nps").contains(dict.getString("custom_qtype"));
        }).collect(Collectors.toList());
    }

    @Override
    public JSONObject getRespondentMqData(SurveyRespondent rspd, JSONObject total_answer, JSONObject custom_answer, Boolean is_copy) {
        if (is_copy == null) {
            is_copy = Boolean.FALSE;
        }
        if (ObjectUtils.isEmpty(rspd)) {
            return null;
        }
        SurveyRespondentWeixin wx_info = surveyRespondentWeixinMapper.selectOne(
                new QueryWrapper<SurveyRespondentWeixin>().lambda()
                        .eq(SurveyRespondentWeixin::getRid, rspd.getId())
                        .last("limit 1"));
        rspd.setWxInfo(wx_info);

        JSONObject mq_data = new JSONObject();
        mq_data.put("project_id", rspd.getProjectId());

        mq_data.put("seq", rspd.getSeq());
        mq_data.put("version", rspd.getVersion());
        mq_data.put("ip", rspd.getIp());
        mq_data.put("ip_country", rspd.getIpCountry());
        mq_data.put("ip_province", rspd.getIpProvince());
        mq_data.put("ip_city", rspd.getIpCity());
        mq_data.put("user_agent", rspd.getUserAgent());
        mq_data.put("status", rspd.getStatus());
        mq_data.put("source", rspd.getSource());
        mq_data.put("data_status", rspd.getDataStatus());
        mq_data.put("time", LocalDateTimeUtil.format(rspd.getCreatedDt(), LOCAL_DATE_TIME_FORMAT));
        mq_data.put("begin_time", LocalDateTimeUtil.format(rspd.getBeginTime(), LOCAL_DATE_TIME_FORMAT));
        mq_data.put("group_id", rspd.getGroupId());
        mq_data.put("respondent_id", rspd.getId());
        //demo数据会有标记 各处订阅处理消息的可以自己选择是否消费这个消息
        mq_data.put("is_copy_demo", rspd.getIsCopyDemo());
        //demo数据和solution数据都会加上这个标记 各处订阅处理消息的可以自己选择是否消费这个消息。true为拷贝数据
        mq_data.put("is_copy", is_copy);

        if (!ObjectUtils.isEmpty(total_answer)) {
            mq_data.put("answers", total_answer);
        }
        if (!ObjectUtils.isEmpty(custom_answer)) {
            mq_data.put("custom_answers", custom_answer);
        }
        if (!ObjectUtils.isEmpty(rspd.getFinishTime())) {
            mq_data.put("finish_time", LocalDateTimeUtil.format(rspd.getFinishTime(), LOCAL_DATE_TIME_FORMAT));
        }
        if (!ObjectUtils.isEmpty(rspd.getGroupCode())) {
            mq_data.put("group_code", rspd.getGroupCode());
        }
        if (!ObjectUtils.isEmpty(rspd.getMemberId())) {
            mq_data.put("member_id", rspd.getMemberId());
        }
        if (!ObjectUtils.isEmpty(rspd.getDeliverId())) {
            mq_data.put("deliver_id", rspd.getDeliverId());
        }
        if (!ObjectUtils.isEmpty(rspd.getDeliverGroupId())) {
            mq_data.put("deliver_group_id", rspd.getDeliverGroupId());
        }
        if (!ObjectUtils.isEmpty(rspd.getAgainDeliverRecordId())) {
            mq_data.put("again_deliver_record_id", rspd.getAgainDeliverRecordId());
        }
        if (!ObjectUtils.isEmpty(wx_info)) {
            JSONObject wx_info_dic = (JSONObject) JSONObject.toJSON(wx_info);
            mq_data.put("channel", wx_info_dic.getString("channel"));
            mq_data.put("user_unique_identification", wx_info_dic.getString("open_id"));
            mq_data.put("wx_nickname", wx_info_dic.getString("wx_nickname"));
            mq_data.put("avatar", wx_info_dic.getString("avatar"));
            mq_data.put("wx_sex", wx_info_dic.getOrDefault("wx_sex", 0));
            mq_data.put("mobile", wx_info_dic.getString("mobile"));
            mq_data.put("email", wx_info_dic.getString("email"));
            mq_data.put("corp_name", wx_info_dic.getString("corp_name"));
            mq_data.put("department", wx_info_dic.getString("department"));
            mq_data.put("position", wx_info_dic.getString("position"));
            mq_data.put("wx_country", wx_info_dic.getString("wx_country"));
            mq_data.put("wx_province", wx_info_dic.getString("wx_province"));
            mq_data.put("wx_city", wx_info_dic.getString("wx_city"));
            mq_data.put("wx_app_id", wx_info_dic.getString("app_id"));
            mq_data.put("wx_open_id", wx_info_dic.getString("open_id"));
            mq_data.put("wx_union_id", wx_info_dic.getString("union_id"));
        }

        if (!ObjectUtils.isEmpty(rspd.getPersonId())) {
            mq_data.put("person_id", rspd.getPersonId());
        }
        if (!ObjectUtils.isEmpty(rspd.getClientType())) {
            mq_data.put("client_type", rspd.getClientType());
        }
        if (!ObjectUtils.isEmpty(rspd.getDlvrRid())) {
            mq_data.put("dlvr_rid", rspd.getDlvrRid());
        }
        log.info("getRespondentMqData mq_data={}", mq_data.toJSONString());
        return mq_data;
    }

    @Override
    public void finishProject(String project_id) throws Exception {
        qdesGrpcClient.finishProject(project_id);
    }

    @Override
    public JSONObject getWxInfoAndWxSessionIdBySid(String sid) {
        String wxsid_key = String.format(WXSID_KEY_FORMAT, sid);
        Object session_byte = redisService.get(wxsid_key);

        JSONObject data = new JSONObject();
        String wx_session_id = "";
        String scope = "";
        if (!ObjectUtils.isEmpty(session_byte)) {
            data = JSONObject.parseObject(JSON.toJSONString(session_byte));
            wx_session_id = data.getString("wx_session_id");
            scope = data.getString("scope");
            if (StrUtil.isNotBlank(wx_session_id)) {
                data = getWxInfoBySessionId(wx_session_id);
            }
        }
        JSONObject result = new JSONObject();
        result.put("wx_user_info", data);
        result.put("wx_session_id", wx_session_id);
        result.put("old_scope", scope);
        return result;
    }

    @Override
    public JSONObject getWxInfoBySessionId(String session_id) {
        String wxsid_key = String.format(WXSID_KEY_FORMAT, session_id);
        Object session_byte = redisService.get(wxsid_key);

        JSONObject data = new JSONObject();

        if (!ObjectUtils.isEmpty(session_byte)) {
            data = JSONObject.parseObject(JSON.toJSONString(session_byte));
        }
        return data;
    }

    @Override
    public JSONObject getUserInfoBySessionId(String sid) {
        return pushGrpcClient.getUserInfoBySessionId(sid);
    }

    @Override
    public void cacheWxuserData(String session_id, JSONObject data, Integer timeout) {
        String wxdata_key = String.format(WXSID_KEY_FORMAT, session_id);
        JSONObject cache_data = new JSONObject();
        if (data != null && !data.isEmpty()) {
            cache_data = data;
        }
        if (timeout != null && timeout > 0) {
            redisService.set(wxdata_key, cache_data, timeout, TimeUnit.SECONDS);
        } else {
            redisService.set(wxdata_key, cache_data);
        }
    }

    @Override
    public void checkDeliverParams(JSONObject params) {
        log.info("checkDeliverParams params={}", params.toJSONString());
        if (ObjectUtils.isEmpty(params)) {
            return;
        }
        String signField = null;
        if (params.containsKey("signature")) {
            signField = "signature";
        }
        Map<String, Object> map = params.toJavaObject(new TypeReference<HashMap<String, Object>>() {
        });
        if (!SignUtil.checkSign(map, deliverSecret, signField)) {
            throw new BusinessException(String.valueOf(ReqErrorCodeEnum.SIGN_ERROR.getCode()), "验签失败");
        }
    }

    private JSONObject getQrDeliverDisable(String deliver_id, String code) {
        return deliverGrpcClient.getQrDeliverDisable(deliver_id, code);
    }

    private JSONObject getDedicatedDeliverDel(String deliver_id, String dedCode) {
        return deliverGrpcClient.getDedicatedDeliverDel(deliver_id, dedCode);
    }

    private boolean checkIpCount(String ip, JSONObject project) {
        log.info("[checkIpCount] ip={},project-customAttr={}", ip, project.getJSONObject("customAttr").toJSONString());
        String id = project.getString("id");
        JSONObject customAttr = project.getJSONObject("customAttr");
        String ipOnce_time = customAttr.getString("ipOnce_time");
        long ipOnce_num = customAttr.getLongValue("ipOnce_num");
        log.info("[checkIpCount] ipOnce_time={},ipOnce_num={}", ipOnce_time, ipOnce_num);

        if (StrUtil.isEmpty(ipOnce_time) && ipOnce_num == 0) {
            return false;
        }
        Long current_ip_count;
        if ("only".equals(ipOnce_time)) {
            log.info("[checkIpCount] only 执行");
            String companyId = project.getString("companyId");
            String companyIdKey = companyId.substring(companyId.length() - 4);
            String key = String.format(SURVEY_CNT_PER_IP_FORMAT_KEY, companyIdKey, id);
            Object o = redisService.hashGet(key, ip);
            if (!ObjectUtils.isEmpty(o)) {
                current_ip_count = Long.parseLong(o.toString());
            } else {
                String key2 = String.format(SURVEY_IP_CNT_FORMAT_KEY, companyIdKey, id);
                if (!redisService.hasKey(key2)) {
                    LambdaQueryWrapper<SurveyRespondent> queryWrapper = Wrappers.lambdaQuery();
                    queryWrapper.eq(SurveyRespondent::getProjectId, id);
                    queryWrapper.gt(SurveyRespondent::getStatus, 0);
                    queryWrapper.eq(SurveyRespondent::getIp, ip);
                    queryWrapper.eq(SurveyRespondent::getDataStatus, RESPONDENT_DATA_STATUS_FORMAL.getValue());
                    current_ip_count = surveyRespondentMapper.selectCount(queryWrapper);
                    if (current_ip_count != null && current_ip_count > 0) {
                        redisService.hashIncr(key, ip, current_ip_count);
                    } else {
                        current_ip_count = 0L;
                    }
                } else {
                    current_ip_count = Long.valueOf("0");
                }
            }

        } else {
            log.info("[checkIpCount] calcTimeRange4CheckCunt 执行，ipOnce_time={}", ipOnce_time);
            Map<String, LocalDateTime> res = calcTimeRange4CheckCunt(ipOnce_time);
            log.info("[checkIpCount] gt_time={},lt_time={}", res.get("gt_time"), res.get("lt_time"));
            if (res.isEmpty()) {
                return false;
            }
            LambdaQueryWrapper<SurveyRespondent> queryWrapper = Wrappers.lambdaQuery();
            queryWrapper.eq(SurveyRespondent::getProjectId, id);
            queryWrapper.gt(SurveyRespondent::getStatus, 0);
            queryWrapper.eq(SurveyRespondent::getIp, ip);
            queryWrapper.eq(SurveyRespondent::getDataStatus, RESPONDENT_DATA_STATUS_FORMAL.getValue());
            queryWrapper.gt(SurveyRespondent::getFinishTime, res.get("gt_time"));
            queryWrapper.lt(SurveyRespondent::getFinishTime, res.get("lt_time"));

            current_ip_count = surveyRespondentMapper.selectCount(queryWrapper);
        }
        log.info("[checkIpCount] current_ip_count={},ipOnce_num={}", current_ip_count, ipOnce_num);
        return current_ip_count >= ipOnce_num;
    }
}




